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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +39 -0
  3. data/.rubocop.yml +11 -5
  4. data/.yardopts +3 -0
  5. data/CHANGELOG.md +34 -1
  6. data/Gemfile +6 -1
  7. data/Gemfile.lock +71 -39
  8. data/README.md +133 -43
  9. data/bin/console +3 -6
  10. data/bin/warren +6 -0
  11. data/lefthook.yml +53 -0
  12. data/lib/sanger_warren.rb +8 -0
  13. data/lib/warren.rb +49 -4
  14. data/lib/warren/app.rb +9 -0
  15. data/lib/warren/app/cli.rb +35 -0
  16. data/lib/warren/app/config.rb +110 -0
  17. data/lib/warren/app/consumer.rb +65 -0
  18. data/lib/warren/app/consumer_add.rb +131 -0
  19. data/lib/warren/app/consumer_start.rb +40 -0
  20. data/lib/warren/app/exchange_config.rb +151 -0
  21. data/lib/warren/app/templates/subscriber.tt +32 -0
  22. data/lib/warren/callback.rb +2 -7
  23. data/lib/warren/callback/broadcast_with_warren.rb +1 -1
  24. data/lib/warren/client.rb +111 -0
  25. data/lib/warren/config/consumers.rb +123 -0
  26. data/lib/warren/delay_exchange.rb +85 -0
  27. data/lib/warren/den.rb +93 -0
  28. data/lib/warren/exceptions.rb +15 -0
  29. data/lib/warren/fox.rb +165 -0
  30. data/lib/warren/framework_adaptor/rails_adaptor.rb +135 -0
  31. data/lib/warren/handler.rb +16 -0
  32. data/lib/warren/handler/base.rb +20 -0
  33. data/lib/warren/handler/broadcast.rb +54 -18
  34. data/lib/warren/handler/log.rb +50 -10
  35. data/lib/warren/handler/test.rb +101 -14
  36. data/lib/warren/helpers/state_machine.rb +55 -0
  37. data/lib/warren/log_tagger.rb +58 -0
  38. data/lib/warren/message.rb +7 -5
  39. data/lib/warren/message/full.rb +20 -0
  40. data/lib/warren/message/short.rb +49 -4
  41. data/lib/warren/message/simple.rb +15 -0
  42. data/lib/warren/railtie.rb +12 -0
  43. data/lib/warren/subscriber/base.rb +151 -0
  44. data/lib/warren/subscription.rb +78 -0
  45. data/lib/warren/version.rb +2 -1
  46. data/sanger-warren.gemspec +5 -4
  47. metadata +49 -6
  48. data/.travis.yml +0 -6
@@ -7,6 +7,22 @@ module Warren
7
7
  # A {Warren::Handler} provides an interface for sending messages to either
8
8
  # a message queue, a log, or an internal store for testing purposes.
9
9
  module Handler
10
+ #
11
+ # Generates a template for routing keys for the given prefix, or a template
12
+ # that returns the provided routing key if no prefix is supplied.
13
+ #
14
+ # @example With a prefix
15
+ # template = Warren::Handler.routing_key_template('example') # => 'example.%s'
16
+ # format(template, 'routing.key') #=> 'example.routing.key'
17
+ #
18
+ # @example Without a prefix
19
+ # template = Warren::Handler.routing_key_template(nil) # => '%s'
20
+ # format(template, 'routing.key') #=> 'routing.key'
21
+ #
22
+ # @param prefix [String, nil] The prefix to use in the template
23
+ #
24
+ # @return [String] A template for generating routing keys
25
+ #
10
26
  def self.routing_key_template(prefix)
11
27
  prefix ? "#{prefix}.%s" : '%s'
12
28
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warren
4
+ module Handler
5
+ # Base class
6
+ class Base
7
+ #
8
+ # Provide API compatibility with the RabbitMQ versions
9
+ # Do nothing in this case
10
+ #
11
+ def connect; end
12
+
13
+ #
14
+ # Provide API compatibility with the RabbitMQ versions
15
+ # Do nothing in this case
16
+ #
17
+ def disconnect; end
18
+ end
19
+ end
20
+ end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bunny'
4
+ require 'forwardable'
5
+ require 'connection_pool'
6
+ require_relative 'base'
4
7
 
5
8
  module Warren
6
9
  module Handler
@@ -8,36 +11,60 @@ module Warren
8
11
  # Class Warren::Broadcast provides a connection pool of
9
12
  # threadsafe RabbitMQ channels for broadcasting messages
10
13
  #
11
- class Broadcast
12
- # Wraps a {Bunny::Channel}
14
+ class Broadcast < Warren::Handler::Base
15
+ # Wraps a Bunny::Channel
16
+ # @see https://rubydoc.info/gems/bunny/Bunny/Channel
13
17
  class Channel
14
- def initialize(bun_channel, routing_key_template:, exchange: nil)
18
+ extend Forwardable
19
+
20
+ attr_reader :routing_key_prefix
21
+
22
+ def_delegators :@bun_channel, :close, :exchange, :queue, :prefetch, :ack, :nack
23
+
24
+ def initialize(bun_channel, routing_key_prefix:, exchange: nil)
15
25
  @bun_channel = bun_channel
16
26
  @exchange_name = exchange
17
- @routing_key_template = routing_key_template
27
+ @routing_key_prefix = routing_key_prefix
28
+ @routing_key_template = Handler.routing_key_template(routing_key_prefix)
18
29
  end
19
30
 
31
+ # Publishes `message` to the configured exchange
32
+ #
33
+ # @param message [#routing_key,#payload] A message should respond to routing_key and payload.
34
+ # @see Warren::Message::Full
35
+ #
36
+ # @return [Warren::Handler::Broadcast::Channel] returns self for chaining
37
+ #
20
38
  def <<(message)
21
- exchange.publish(message.payload, routing_key: key_for(message))
22
- self
39
+ publish(message)
23
40
  end
24
41
 
25
- def close
26
- @bun_channel.close
42
+ # Publishes `message` to `exchange` (Defaults to configured exchange)
43
+ #
44
+ # @param message [#routing_key,#payload] A message should respond to routing_key and payload.
45
+ # @see Warren::Message::Full
46
+ # @param exchange [Bunny::Exchange] The exchange to publish to
47
+ #
48
+ # @return [Warren::Handler::Broadcast::Channel] returns self for chaining
49
+ #
50
+ def publish(message, exchange: configured_exchange)
51
+ exchange.publish(message.payload, routing_key: key_for(message), headers: message.headers)
52
+ self
27
53
  end
28
54
 
29
55
  private
30
56
 
31
- def exchange
57
+ def configured_exchange
32
58
  raise StandardError, 'No exchange configured' if @exchange_name.nil?
33
59
 
34
- @exchange ||= @bun_channel.topic(@exchange_name, auto_delete: false, durable: true)
60
+ @configured_exchange ||= exchange(@exchange_name, auto_delete: false, durable: true, type: :topic)
35
61
  end
36
62
 
37
63
  def key_for(message)
38
64
  @routing_key_template % message.routing_key
39
65
  end
40
66
  end
67
+
41
68
  #
42
69
  # Creates a warren but does not connect.
43
70
  #
@@ -47,10 +74,11 @@ module Warren
47
74
  # @param [String,nil] routing_key_prefix The prefix to pass before the routing key.
48
75
  # Can be used to ensure environments remain distinct.
49
76
  def initialize(exchange:, routing_key_prefix:, server: {}, pool_size: 14)
77
+ super()
50
78
  @server = server
51
79
  @exchange_name = exchange
52
80
  @pool_size = pool_size
53
- @routing_key_template = Handler.routing_key_template(routing_key_prefix)
81
+ @routing_key_prefix = routing_key_prefix
54
82
  end
55
83
 
56
84
  #
@@ -74,12 +102,12 @@ module Warren
74
102
  end
75
103
 
76
104
  #
77
- # Yields an exchange which gets returned to the pool on block closure
78
- #
105
+ # Yields an {Warren::Handler::Broadcast::Channel} which gets returned to the pool on block closure
79
106
  #
80
107
  # @return [void]
81
108
  #
82
- # @yieldreturn [Warren::Broadcast::Channel] A rabbitMQ channel that sends messages to the configured exchange
109
+ # @yieldparam [Warren::Handler::Broadcast::Channel] A rabbitMQ channel that sends messages to the configured
110
+ # exchange
83
111
  def with_channel(&block)
84
112
  connection_pool.with(&block)
85
113
  end
@@ -90,7 +118,7 @@ module Warren
90
118
  #
91
119
  # @param [Warren::Message] message The message to broadcast. Must respond to #routing_key and #payload
92
120
  #
93
- # @return [Warren::Broadcast] Returns itself to allow chaining. But you're
121
+ # @return [Warren::Handler::Broadcast] Returns itself to allow chaining. But you're
94
122
  # probably better off using #with_channel
95
123
  # in that case
96
124
  #
@@ -99,16 +127,24 @@ module Warren
99
127
  self
100
128
  end
101
129
 
130
+ def new_channel(worker_count: 1)
131
+ Channel.new(session.create_channel(nil, worker_count), exchange: @exchange_name,
132
+ routing_key_prefix: @routing_key_prefix)
133
+ end
134
+
102
135
  private
103
136
 
137
+ def server_connection
138
+ ENV.fetch('WARREN_CONNECTION_URI', @server)
139
+ end
140
+
104
141
  def session
105
- @session ||= Bunny.new(@server)
142
+ @session ||= Bunny.new(server_connection)
106
143
  end
107
144
 
108
145
  def connection_pool
109
146
  @connection_pool ||= start_session && ConnectionPool.new(size: @pool_size, timeout: 5) do
110
- Channel.new(session.create_channel, exchange: @exchange_name,
111
- routing_key_template: @routing_key_template)
147
+ new_channel
112
148
  end
113
149
  end
114
150
 
@@ -1,23 +1,45 @@
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::Log provides a dummy RabbitMQ
6
8
  # connection pool for use during development
7
- class Log
8
- # Mimics a {Bunny::Channel} but instead passes out to a logger
9
+ class Log < Warren::Handler::Base
10
+ # Mimics a {Broadcast::Channel} but instead passes out to a logger
9
11
  class Channel
10
12
  def initialize(logger, routing_key_template: '%s')
11
13
  @logger = logger
12
14
  @routing_key_template = routing_key_template
13
15
  end
14
16
 
17
+ # Logs `message` to the configured logger
18
+ #
19
+ # @param message [#routing_key,#payload] A message should respond to routing_key and payload.
20
+ # @see Warren::Message::Full
21
+ #
22
+ # @return [Warren::Handler::Broadcast::Channel] returns self for chaining
23
+ #
15
24
  def <<(message)
16
25
  @logger.info "Published: #{key_for(message)}"
17
26
  @logger.debug "Payload: #{message.payload}"
18
27
  self
19
28
  end
20
29
 
30
+ def exchange(name, options)
31
+ @logger.debug "Declared exchange: #{name}, #{options.inspect}"
32
+ Exchange.new(name, options)
33
+ end
34
+
35
+ def queue(name, options)
36
+ @logger.debug "Declared queue: #{name}, #{options.inspect}"
37
+ Queue.new(@logger, name)
38
+ end
39
+
40
+ # NOOP - Provided for API compatibility
41
+ def prefetch(number); end
42
+
21
43
  private
22
44
 
23
45
  def key_for(message)
@@ -25,20 +47,38 @@ module Warren
25
47
  end
26
48
  end
27
49
 
50
+ # Small object to track exchange properties for logging purposes
51
+ Exchange = Struct.new(:name, :options)
52
+
53
+ # Queue class to provide extended logging in development mode
54
+ class Queue
55
+ def initialize(logger, name)
56
+ @logger = logger
57
+ @name = name
58
+ end
59
+
60
+ def bind(exchange, options)
61
+ @logger.debug "Bound queue #{@name}: #{exchange}, #{options.inspect}"
62
+ end
63
+
64
+ def subscribe(options)
65
+ @logger.debug "Subscribed to queue #{@name}: #{options.inspect}"
66
+ @logger.warn 'This is a Warren::Handler::Log no messages will be processed'
67
+ nil
68
+ end
69
+ end
70
+
28
71
  attr_reader :logger
29
72
 
30
73
  def initialize(logger:, routing_key_prefix: nil)
74
+ super()
31
75
  @logger = logger
32
76
  @routing_key_template = Handler.routing_key_template(routing_key_prefix)
33
77
  end
34
78
 
35
- #
36
- # Provide API compatibility with the RabbitMQ versions
37
- # Do nothing in this case
38
- #
39
- def connect; end
40
-
41
- def disconnect; end
79
+ def new_channel
80
+ Channel.new(@logger, routing_key_template: @routing_key_template)
81
+ end
42
82
 
43
83
  #
44
84
  # Yields a Warren::Log::Channel
@@ -48,7 +88,7 @@ module Warren
48
88
  #
49
89
  # @yieldreturn [Warren::Log::Channel] A rabbitMQ channel that logs messaged to the test warren
50
90
  def with_channel
51
- yield Channel.new(@logger, routing_key_template: @routing_key_template)
91
+ yield new_channel
52
92
  end
53
93
 
54
94
  #
@@ -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
- class Test
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,17 +70,25 @@ 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 {Bunny::Channel}, provides a store of messages to use
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)
30
77
  @warren = warren
31
78
  end
32
79
 
80
+ # Records `message` for testing purposes
81
+ #
82
+ # @param message [#routing_key,#payload] A message should respond to routing_key and payload.
83
+ # @see Warren::Message::Full
84
+ #
85
+ # @return [Warren::Handler::Broadcast::Channel] returns self for chaining
86
+ #
33
87
  def <<(message)
34
88
  @warren << message
35
89
  end
36
90
  end
91
+
37
92
  #
38
93
  # Creates a test warren with no messages.
39
94
  # Test warrens are shared across all threads.
@@ -41,45 +96,74 @@ module Warren
41
96
  # @param [_] _args Configuration arguments are ignored.
42
97
  #
43
98
  def initialize(*_args)
99
+ super()
44
100
  @messages = []
101
+ @exchanges = []
45
102
  @enabled = false
46
103
  end
47
104
 
48
105
  #
49
- # Provide API compatibility with the RabbitMQ versions
50
- # Do nothing in this case
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
- #
106
+ # Yields a new channel, which proxies all message back to {messages} on the
107
+ # {Warren::Handler::Test}
59
108
  #
60
109
  # @return [void]
61
110
  #
62
111
  # @yieldreturn [Warren::Test::Channel] A rabbitMQ channel that logs messaged to the test warren
63
112
  def with_channel
64
- yield Channel.new(self)
113
+ yield new_channel
65
114
  end
66
115
 
116
+ #
117
+ # Returns a new channel, which proxies all message back to {messages} on the
118
+ # {Warren::Handler::Test}
119
+ #
120
+ # @return [Warren::Test::Channel] A rabbitMQ channel that logs messaged to the test warren
121
+ #
122
+ def new_channel
123
+ Channel.new(@logger, routing_key_template: @routing_key_template)
124
+ end
125
+
126
+ #
127
+ # Clear any logged messaged
128
+ #
129
+ # @return [Array] The new empty array, lacking messages
130
+ #
67
131
  def clear_messages
68
132
  @messages = []
133
+ @exchanges = []
69
134
  end
70
135
 
136
+ #
137
+ # Returns the last message received by the warren
138
+ #
139
+ # @return [#routing_key#payload] The last message object received by the warren
140
+ #
71
141
  def last_message
72
142
  messages.last
73
143
  end
74
144
 
145
+ #
146
+ # Returns the total number message received by the warren since it was enabled
147
+ #
148
+ # @return [Integer] The total number of messages
149
+ #
75
150
  def message_count
76
151
  messages.length
77
152
  end
78
153
 
154
+ #
155
+ # Returns the total number message received by the warren matching the given
156
+ # routing_key since it was enabled
157
+ #
158
+ # @param routing_key [String] The routing key to filter by
159
+ #
160
+ # @return [Integer] The number of matching messages
161
+ #
79
162
  def messages_matching(routing_key)
80
163
  messages.count { |message| message.routing_key == routing_key }
81
164
  end
82
165
 
166
+ # Enable the warren
83
167
  def enable!
84
168
  @enabled = true
85
169
  clear_messages
@@ -91,6 +175,9 @@ module Warren
91
175
  clear_messages
92
176
  end
93
177
 
178
+ # Returns an array of all message received by the warren since it was enabled
179
+ #
180
+ # @return [Array<#routing_key#payload>] All received messages
94
181
  def messages
95
182
  raise_if_not_tracking
96
183
  @messages