sanger_warren 0.2.0.rc1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6f14e9809f8c71405a3696920321ebbf7477c7f0ecf05421a3ffd85f4fbccfc
4
- data.tar.gz: 5e13bbba217f9b09ad08433178e03f822a6f97e207c80e532a5c8b15c8d36b7b
3
+ metadata.gz: bdb0a5a3497a84b68ad0be1b01bfb80167452b6a3209badf12d5bba8cb9e3390
4
+ data.tar.gz: 8271f438b3ad81320a3a1530292edefdc66a40b4e858a908568b151f22a3ae3c
5
5
  SHA512:
6
- metadata.gz: 0e2966ea368981ab85de2fc6ed22e349af14f2178862ee0025c79699a494e1d857d96eb23ab1c5fe5a68a24c9f6db68254c488add1d181b9391dc6e17156ca32
7
- data.tar.gz: 8e882eba3c9a5e450447c73784bbed94655bf599c086521c0298a3f9a45433fca703ac52660644b262d837b94f0e793f3393e461dc62d96e76b99cffb1c64a6b
6
+ metadata.gz: 517e0e58370945acc610974a4657fc424ee2483d4e794ce74ebe026580aad2fa47e23643304cedc6df99e6354e1a06a430387f70da71c877ec80bc022c7b3118
7
+ data.tar.gz: 6bbdfe62aa22ea3bc42106800c79fe1f8b3a16a5d50898479ce17ab1b1bf3854ce02a65a83d641d07295dfb138df5ae2d43a60762a6f82c40f10cc1054877579
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ LICENSE.txt
3
+ CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ Unreleased section to make new releases easy.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.4.0] - 2021-06-09
9
+
10
+ ### Fixed
11
+
12
+ - Ensured backwards compatibility with 0.2.0
13
+
14
+ ## [0.3.0] - 2021-06-04
15
+
16
+ ### Added
17
+
18
+ - Added support for delay exchanges to process messages after a fixed delay
19
+ - Increased documentation
20
+ - Added Warren::Message::Simple for wrapping just routing key and payload.
21
+ - Added optional worker_count to warren_consumers.yml to control number of worker threads
22
+
23
+ ### Removed
24
+
25
+ - Warren::Handler::Test and Warren::Handler::Test::Channel no loner respond to
26
+ `add_exchange`. These methods were undocumented, and unused internally.
27
+
28
+ ## Changed
29
+
30
+ - Messages must now implement `#headers`, although simply returning an empty
31
+ hash is sufficient.
32
+ See {Warren::Message::Simple#headers} for example
33
+ - Subscriber templates now use the path 'app/warren/subscriber' rather than
34
+ 'app/warren/subscribers' to correctly match class namespacing.
35
+ - 3 consumer worker threads will be spun up by default
36
+
37
+ ## [0.2.0]
38
+
8
39
  ### Added
9
40
 
10
41
  - Added railties to automatically initialize and configure Warren in rails apps.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sanger_warren (0.2.0.rc1)
4
+ sanger_warren (0.4.0)
5
5
  bunny (~> 2.17.0)
6
6
  connection_pool (~> 2.2.0)
7
7
  multi_json (~> 1.0)
@@ -10,13 +10,13 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- activemodel (5.2.5)
14
- activesupport (= 5.2.5)
15
- activerecord (5.2.5)
16
- activemodel (= 5.2.5)
17
- activesupport (= 5.2.5)
13
+ activemodel (5.2.6)
14
+ activesupport (= 5.2.6)
15
+ activerecord (5.2.6)
16
+ activemodel (= 5.2.6)
17
+ activesupport (= 5.2.6)
18
18
  arel (>= 9.0)
19
- activesupport (5.2.5)
19
+ activesupport (5.2.6)
20
20
  concurrent-ruby (~> 1.0, >= 1.0.2)
21
21
  i18n (>= 0.7, < 2)
22
22
  minitest (~> 5.1)
@@ -28,24 +28,24 @@ GEM
28
28
  amq-protocol (~> 2.3, >= 2.3.1)
29
29
  coderay (1.1.3)
30
30
  concurrent-ruby (1.1.8)
31
- connection_pool (2.2.3)
31
+ connection_pool (2.2.5)
32
32
  diff-lcs (1.4.4)
33
- docile (1.3.5)
33
+ docile (1.4.0)
34
34
  i18n (1.8.10)
35
35
  concurrent-ruby (~> 1.0)
36
36
  method_source (1.0.0)
37
37
  minitest (5.14.4)
38
38
  multi_json (1.15.0)
39
39
  parallel (1.20.1)
40
- parser (3.0.0.0)
40
+ parser (3.0.1.1)
41
41
  ast (~> 2.4.1)
42
- pry (0.14.0)
42
+ pry (0.14.1)
43
43
  coderay (~> 1.1)
44
44
  method_source (~> 1.0)
45
45
  rainbow (3.0.0)
46
46
  rake (13.0.3)
47
47
  regexp_parser (2.1.1)
48
- rexml (3.2.4)
48
+ rexml (3.2.5)
49
49
  rspec (3.10.0)
50
50
  rspec-core (~> 3.10.0)
51
51
  rspec-expectations (~> 3.10.0)
@@ -59,20 +59,20 @@ GEM
59
59
  diff-lcs (>= 1.2.0, < 2.0)
60
60
  rspec-support (~> 3.10.0)
61
61
  rspec-support (3.10.2)
62
- rubocop (1.11.0)
62
+ rubocop (1.15.0)
63
63
  parallel (~> 1.10)
64
64
  parser (>= 3.0.0.0)
65
65
  rainbow (>= 2.2.2, < 4.0)
66
66
  regexp_parser (>= 1.8, < 3.0)
67
67
  rexml
68
- rubocop-ast (>= 1.2.0, < 2.0)
68
+ rubocop-ast (>= 1.5.0, < 2.0)
69
69
  ruby-progressbar (~> 1.7)
70
70
  unicode-display_width (>= 1.4.0, < 3.0)
71
- rubocop-ast (1.4.1)
72
- parser (>= 2.7.1.5)
71
+ rubocop-ast (1.5.0)
72
+ parser (>= 3.0.1.1)
73
73
  rubocop-rake (0.5.1)
74
74
  rubocop
75
- rubocop-rspec (2.2.0)
75
+ rubocop-rspec (2.3.0)
76
76
  rubocop (~> 1.0)
77
77
  rubocop-ast (>= 1.1.0)
78
78
  ruby-progressbar (1.11.0)
@@ -81,7 +81,7 @@ GEM
81
81
  simplecov-html (~> 0.11)
82
82
  simplecov_json_formatter (~> 0.1)
83
83
  simplecov-html (0.12.3)
84
- simplecov_json_formatter (0.1.2)
84
+ simplecov_json_formatter (0.1.3)
85
85
  thor (1.1.0)
86
86
  thread_safe (0.3.6)
87
87
  tzinfo (1.2.9)
data/README.md CHANGED
@@ -32,6 +32,13 @@ If using with a Rails app, you can simply run `bundle exec warren config` to
32
32
  help generate a warren config file. Warren will automatically be initialize
33
33
  on Rails start-up.
34
34
 
35
+ In rails 5 you will need to add the following to your `config/application.rb`
36
+ to ensure the auto-loader can find the subscribers.
37
+
38
+ ```ruby
39
+ config.autoload_paths += %W{#{Rails.root}/app}
40
+ ```
41
+
35
42
  ### Handler types
36
43
 
37
44
  In development mode, warren is usually configured to log to the console only. If
@@ -97,6 +104,28 @@ These options can be over-ridden in the warren_consumers.yml file if necessary.
97
104
  If you wish to completely disable dead-letter queue configuration, such as when
98
105
  using policies, then you can set dead_letters to false.
99
106
 
107
+ #### Delayed messaged
108
+
109
+ Warren uses a delay exchange / queue approach for delaying the redelivery of messages.
110
+ We don't currently support the
111
+ [delayed message exchange plugin](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange)
112
+
113
+ The way this works:
114
+
115
+ - Consumers with a delay option will automatically register an exchange and a queue
116
+ - By default both of these will be named '<consumer_name>.delay' although you can
117
+ override this by updating `warren_consumers.yml`
118
+ - The '<consumer_name>.delay' queue will have a time-to-live (ttl) set, after
119
+ which any messages on the queue will be dead-lettered
120
+ - The dead-letter exchange for this queue is configured to an empty string `''`
121
+ which corresponds to the 'default exchange', and the routing key is set to
122
+ match the original queue name.
123
+ - This exchange is a little bit special, and automatically routes messages to
124
+ any queue on the host where the queue name matches the message routing key.
125
+
126
+ This approach avoids repeat delivery of the message to any other queues bound
127
+ to the original exchange, and avoids the need for further exchanges or bindings.
128
+
100
129
  ### Running consumers
101
130
 
102
131
  To run all configure consumers use:
data/lefthook.yml CHANGED
@@ -7,7 +7,7 @@ pre-commit:
7
7
  commands:
8
8
  rubocop:
9
9
  glob: '{*.{rb,arb,axlsx,builder,fcgi,gemfile,gemspec,god,jb,jbuilder,mspec,opal,pluginspec,podspec,rabl,rake,rbuild,rbw,rbx,ru,ruby,spec,thor,watchr},.irbrc,.pryrc,.simplecov,buildfile,Appraisals,Berksfile,Brewfile,Buildfile,Capfile,Cheffile,Dangerfile,Deliverfile,Fastfile,*Fastfile,Gemfile,Guardfile,Jarfile,Mavenfile,Podfile,Puppetfile,Rakefile,rakefile,Snapfile,Steepfile,Thorfile,Vagabondfile,Vagrantfile}'
10
- run: rubocop --display-style-guide --extra-details --force-exclusion --parallel {staged_files} || (echo 'Run `lefthook run fix` to run autocrrect on staged files only'; exit 1)
10
+ run: rubocop --display-style-guide --extra-details --force-exclusion --parallel {staged_files} || (echo 'Run `lefthook run fix` to run autocorrect on staged files only'; exit 1)
11
11
 
12
12
  fix:
13
13
  parallel: true
@@ -23,6 +23,7 @@ module Warren
23
23
  desc: 'The path to the configuration file to generate'
24
24
  option :exchange, type: :string,
25
25
  desc: 'The RabbitMQ exchange to connect to'
26
+ # Invoked by `$ warren config` generates a `warren.yml` file.
26
27
  def config
27
28
  Warren::App::Config.invoke(self, path: options['path'], exchange: options['exchange'])
28
29
  end
@@ -42,6 +42,16 @@ module Warren
42
42
  # circumstances should you commit sensitive information in the file.
43
43
  TEMPLATE
44
44
 
45
+ # Triggers the configuration task. Primarily called by the Thor CLI.
46
+ # Will either use arguments passed in from the command line, or prompt the
47
+ # user for them if missing.
48
+ #
49
+ # @param shell [Thor::Shell::Basic] Thor shell instance for feedback
50
+ # @param path [String] Path to the `warren.yml` file
51
+ # @param exchange [String, nil] Name of the exchange to use, if passed in from CLI
52
+ #
53
+ # @return [Void]
54
+ #
45
55
  def self.invoke(shell, path:, exchange: nil)
46
56
  new(shell, path: path, exchange: exchange).invoke
47
57
  end
@@ -32,6 +32,14 @@ module Warren
32
32
  option :path, type: :string,
33
33
  default: Warren::Config::Consumers::DEFAULT_PATH,
34
34
  desc: 'The path to the consumer configuration file to generate'
35
+ option :delay, type: :numeric,
36
+ desc: 'The delay (ms) on the delay queue. 0 to skip queue creation.'
37
+ # Invoked by `$ warren consumer add` adds a consumer to the `warren_consumers.yml`
38
+ #
39
+ # @param name [String, nil] Optional: Passed in from Command. The name of the consumer to create.
40
+ #
41
+ # @return [Void]
42
+ #
35
43
  def add(name = nil)
36
44
  say 'Adding a consumer'
37
45
  Warren::App::ConsumerAdd.invoke(self, name, options)
@@ -44,6 +52,10 @@ module Warren
44
52
  option :consumers, type: :array,
45
53
  desc: 'The consumers to start. Defaults to all consumers',
46
54
  banner: 'consumer_name other_consumer'
55
+ # Invoked by `$ warren consumer start`. Starts up the configured consumers
56
+ #
57
+ # @return [Void]
58
+ #
47
59
  def start
48
60
  say 'Starting consumers'
49
61
  Warren::App::ConsumerStart.invoke(self, options)
@@ -7,7 +7,8 @@ module Warren
7
7
  module App
8
8
  # Handles the initial creation of the configuration object
9
9
  class ConsumerAdd
10
- SUBSCRIBER_NAMESPACE = 'Warren::Subscriber::'
10
+ # Default namespace for new Subscribers
11
+ SUBSCRIBER_NAMESPACE = %w[Warren Subscriber].freeze
11
12
 
12
13
  attr_reader :name, :desc, :queue
13
14
 
@@ -46,6 +47,7 @@ module Warren
46
47
  @name = name
47
48
  @desc = options[:desc]
48
49
  @queue = options[:queue]
50
+ @delay = options[:delay]
49
51
  @config = Warren::Config::Consumers.new(options[:path])
50
52
  @bindings = Warren::App::ExchangeConfig.parse(shell, options[:bindings])
51
53
  end
@@ -69,7 +71,7 @@ module Warren
69
71
  def subscribed_class
70
72
  class_name = name.split(/[\s\-_]/).map(&:capitalize).join
71
73
 
72
- "#{SUBSCRIBER_NAMESPACE}#{class_name}"
74
+ [*SUBSCRIBER_NAMESPACE, class_name].join('::')
73
75
  end
74
76
 
75
77
  def check_name
@@ -98,6 +100,9 @@ module Warren
98
100
  @desc ||= @shell.ask 'Provide an optional description: '
99
101
  @queue ||= @shell.ask 'Provide the name of the queue to connect to: '
100
102
  @bindings ||= gather_bindings
103
+ @delay ||= @shell.ask(
104
+ 'Create a delay queue? Specify delay in milliseconds to create; set to 0 or leave blank to skip.'
105
+ ).to_i
101
106
  nil
102
107
  end
103
108
 
@@ -106,16 +111,20 @@ module Warren
106
111
  end
107
112
 
108
113
  def write_configuration
109
- @config.add_consumer(@name, desc: @desc, queue: @queue, bindings: @bindings, subscribed_class: subscribed_class)
114
+ @config.add_consumer(
115
+ @name, desc: @desc, queue: @queue,
116
+ bindings: @bindings, subscribed_class: subscribed_class,
117
+ delay: @delay
118
+ )
110
119
  @config.save
111
120
  end
112
121
 
113
122
  def write_subscriber
114
- @shell.template('subscriber.tt', consumer_path, context: binding)
123
+ @shell.template('subscriber.tt', subscriber_path, context: binding)
115
124
  end
116
125
 
117
- def consumer_path
118
- "app/warren/subscribers/#{@name.tr(' -', '_')}.rb"
126
+ def subscriber_path
127
+ "#{['app', *SUBSCRIBER_NAMESPACE, @name.tr(' -', '_')].map(&:downcase).join('/')}.rb"
119
128
  end
120
129
  end
121
130
  end
@@ -7,6 +7,17 @@ module Warren
7
7
  module App
8
8
  # Handles the initial creation of the configuration object
9
9
  class ConsumerStart
10
+ #
11
+ # Starts up a warren client process for the configured consumers.
12
+ #
13
+ # @param shell [Thor::Shell::Basic] Thor shell instance for feedback
14
+ # @param options [Hash] Hash of command line arguments from Thor
15
+ # @option options [String] :path Path to the `warren_consumers.yml `file
16
+ # @option options [Array<String>] :consumers Array of configured consumers to start.
17
+ # Defaults to all consumers
18
+ #
19
+ # @return [Void]
20
+ #
10
21
  def self.invoke(shell, options)
11
22
  new(shell, options).invoke
12
23
  end
@@ -17,6 +28,10 @@ module Warren
17
28
  @consumers = options[:consumers]
18
29
  end
19
30
 
31
+ #
32
+ # Starts up a warren client process for the configured consumers.
33
+ #
34
+ # @return [Void]
20
35
  def invoke
21
36
  Warren::Client.new(@config, consumers: @consumers).run
22
37
  end
@@ -102,6 +102,7 @@ module Warren
102
102
 
103
103
  def ask_direct_binding
104
104
  exchange = ask_exchange
105
+ routing_key_tip
105
106
  routing_key = @shell.ask 'Specify a routing_key: '
106
107
  add_binding('direct', exchange, { routing_key: routing_key })
107
108
  end
@@ -119,6 +120,7 @@ module Warren
119
120
 
120
121
  def ask_topic_binding
121
122
  exchange = ask_exchange
123
+ routing_key_tip
122
124
  loop do
123
125
  routing_key = @shell.ask 'Specify a routing_key [Leave blank to stop adding]: '
124
126
  break if routing_key == ''
@@ -133,6 +135,17 @@ module Warren
133
135
  'options' => options
134
136
  }
135
137
  end
138
+
139
+ def routing_key_tip
140
+ # Suggested cop style of %<routing_key_prefix>s but prefer suggesting the simpler option as it
141
+ # would be all to easy to miss out the 's', resulting in varying behaviour depending on the following
142
+ # character
143
+ # rubocop:disable Style/FormatStringToken
144
+ @shell.say(
145
+ 'Tip: Use %{routing_key_prefix} in routing keys to reference the routing_key_prefix specified in warren.yml'
146
+ )
147
+ # rubocop:enable Style/FormatStringToken
148
+ end
136
149
  end
137
150
  end
138
151
  end
@@ -15,7 +15,7 @@ module Warren
15
15
  # Creates the callback object
16
16
  #
17
17
  # @param handler [Warren::Handler] The handler to take the messaged
18
- # @param message_class [Warren::Message] The adpater to render the messages
18
+ # @param message_class [Warren::Message] The adaptor to render the messages
19
19
  #
20
20
  def initialize(handler:, message_class: Warren::Message::Short)
21
21
  @handler = handler
@@ -60,10 +60,12 @@ module Warren
60
60
  # @param desc [String] Description of the consumer (Primarily for documentation)
61
61
  # @param queue [String] Name of the queue to attach to
62
62
  # @param bindings [Array<Hash>] Array of binding configuration hashed
63
+ # @param delay [Integer] Delay on the generated delay exchange
63
64
  #
64
65
  # @return [Hash] The consumer configuration hash
65
66
  #
66
- def add_consumer(name, desc:, queue:, bindings:, subscribed_class:)
67
+ # rubocop:todo Metrics/ParameterLists
68
+ def add_consumer(name, desc:, queue:, bindings:, subscribed_class:, delay:)
67
69
  dead_letter_exchange = "#{name}.dead-letters"
68
70
  @config[name] = {
69
71
  'desc' => desc,
@@ -71,9 +73,12 @@ module Warren
71
73
  'subscribed_class' => subscribed_class,
72
74
  # This smells wrong. I don't like the call back out to the App namespace
73
75
  'dead_letters' => queue_config(dead_letter_exchange,
74
- Warren::App::ExchangeConfig.default_dead_letter(dead_letter_exchange))
76
+ Warren::App::ExchangeConfig.default_dead_letter(dead_letter_exchange)),
77
+ 'delay' => delay_exchange_configuration(ttl: delay, original_queue: queue, consumer_name: name),
78
+ 'worker_count' => 3
75
79
  }
76
80
  end
81
+ # rubocop:enable Metrics/ParameterLists
77
82
 
78
83
  private
79
84
 
@@ -86,6 +91,23 @@ module Warren
86
91
  }
87
92
  end
88
93
 
94
+ # rubocop:todo Metrics/MethodLength
95
+ def delay_exchange_configuration(ttl:, original_queue:, consumer_name:)
96
+ return {} if ttl.nil? || ttl.zero?
97
+
98
+ {
99
+ 'exchange' => { 'name' => "#{consumer_name}.delay", 'options' => { type: 'fanout', durable: true } },
100
+ 'bindings' => [{
101
+ 'queue' => { 'name' => "#{consumer_name}.delay", 'options' => {
102
+ durable: true, arguments: {
103
+ 'x-dead-letter-exchange' => '', 'x-message-ttl' => ttl, 'x-dead-letter-routing-key' => original_queue
104
+ }
105
+ } }, 'options' => {}
106
+ }]
107
+ }
108
+ end
109
+ # rubocop:enable Metrics/MethodLength
110
+
89
111
  #
90
112
  # Loads the configuration, should be a hash
91
113
  #
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warren
4
+ # Configures and wraps up delay exchange on a Bunny Channel/Queue
5
+ # A delay exchange routes immediately onto a queue with a ttl
6
+ # once messages on this queue expire they are dead-lettered back onto
7
+ # to original exchange
8
+ # Note: This does not currently support the rabbitmq-delayed-message-exchange
9
+ # plugin.
10
+ class DelayExchange
11
+ extend Forwardable
12
+
13
+ attr_reader :channel
14
+
15
+ #
16
+ # Create a new delay exchange. Handles queue creation, binding and attaching
17
+ # consumers to the queues
18
+ #
19
+ # @param channel [Warren::Handler::Broadcast::Channel] A channel on which to register queues
20
+ # @param config [Hash] queue configuration hash
21
+ #
22
+ def initialize(channel:, config:)
23
+ @channel = channel
24
+ @exchange_config = config&.fetch('exchange', nil)
25
+ @bindings = config&.fetch('bindings', []) || []
26
+ end
27
+
28
+ def_delegators :channel, :nack, :ack
29
+
30
+ # Ensures the queues and channels are set up to receive messages
31
+ # keys: additional routing_keys to bind
32
+ def activate!
33
+ establish_bindings!
34
+ end
35
+
36
+ #
37
+ # Post a message to the delay exchange.
38
+ #
39
+ # @param payload [String] The message payload
40
+ # @param routing_key [String] The routing key of the re-sent message
41
+ # @param headers [Hash] A hash of headers. Typically: { attempts: <Integer> }
42
+ # @option headers [Integer] :attempts The number of times the message has been processed
43
+ #
44
+ # @return [Void]
45
+ #
46
+ def publish(payload, routing_key:, headers: {})
47
+ raise StandardError, 'No delay queue configured' unless configured?
48
+
49
+ message = Warren::Message::Simple.new(routing_key, payload, headers)
50
+ channel.publish(message, exchange: exchange)
51
+ end
52
+
53
+ private
54
+
55
+ def configured?
56
+ @exchange_config&.key?('name')
57
+ end
58
+
59
+ def add_binding(queue, options)
60
+ queue.bind(exchange, options)
61
+ end
62
+
63
+ def exchange
64
+ @exchange ||= channel.exchange(*@exchange_config.values_at('name', 'options'))
65
+ end
66
+
67
+ def queue(config)
68
+ channel.queue(*config.values_at('name', 'options'))
69
+ end
70
+
71
+ def establish_bindings!
72
+ @bindings.each do |binding_config|
73
+ queue = queue(binding_config['queue'])
74
+ transformed_options = merge_routing_key_prefix(binding_config['options'])
75
+ add_binding(queue, transformed_options)
76
+ end
77
+ end
78
+
79
+ def merge_routing_key_prefix(options)
80
+ options.transform_values do |value|
81
+ format(value, routing_key_prefix: channel.routing_key_prefix)
82
+ end
83
+ end
84
+ end
85
+ end
data/lib/warren/den.rb CHANGED
@@ -3,12 +3,17 @@
3
3
  require 'bunny'
4
4
  require 'warren/fox'
5
5
  require 'warren/subscription'
6
+ require 'warren/delay_exchange'
6
7
 
7
8
  module Warren
8
9
  # A Den is in charge of creating a Fox from a consumer configuration
9
- # Currently its pretty simple, but in future will also handle registration of
10
- # delay and dead-letter queues/exchanges.
10
+ # It handles the registration of dead-letter queues, and configuration of
11
+ # {Warren::Subscription subscriptions} and
12
+ # {Warren::DelayExchange delay exchanges}
11
13
  class Den
14
+ # The number of simultaneous workers generated by default
15
+ DEFAULT_WORKER_COUNT = 3
16
+
12
17
  #
13
18
  # Create a {Warren::Fox} work pool.
14
19
  # @param app_name [String] The name of the application. Corresponds to the
@@ -34,9 +39,10 @@ module Warren
34
39
  config = dead_letter_config
35
40
  return unless config
36
41
 
37
- channel = Warren.handler.new_channel
38
- subscription = Warren::Subscription.new(channel: channel, config: config)
39
- subscription.activate!
42
+ Warren.handler.with_channel do |channel|
43
+ subscription = Warren::Subscription.new(channel: channel, config: config)
44
+ subscription.activate!
45
+ end
40
46
  end
41
47
 
42
48
  private
@@ -54,12 +60,18 @@ module Warren
54
60
  # and while we *can* share channels between consumers it results in them
55
61
  # sharing the same worker pool. This process lets us control workers on
56
62
  # a per-queue basis. Currently that just means one worker per consumer.
57
- channel = Warren.handler.new_channel
63
+ channel = Warren.handler.new_channel(worker_count: worker_count)
58
64
  subscription = Warren::Subscription.new(channel: channel, config: queue_config)
65
+ delay = Warren::DelayExchange.new(channel: channel, config: delay_config)
59
66
  Warren::Fox.new(name: @app_name,
60
67
  subscription: subscription,
61
68
  adaptor: @adaptor,
62
- subscribed_class: subscribed_class)
69
+ subscribed_class: subscribed_class,
70
+ delayed: delay)
71
+ end
72
+
73
+ def worker_count
74
+ consumer_config.fetch('worker_count', DEFAULT_WORKER_COUNT)
63
75
  end
64
76
 
65
77
  def queue_config
@@ -70,6 +82,10 @@ module Warren
70
82
  consumer_config.fetch('dead_letters')
71
83
  end
72
84
 
85
+ def delay_config
86
+ consumer_config.fetch('delay', nil)
87
+ end
88
+
73
89
  def subscribed_class
74
90
  Object.const_get(consumer_config.fetch('subscribed_class'))
75
91
  end
data/lib/warren/fox.rb CHANGED
@@ -20,7 +20,7 @@ module Warren
20
20
  # Maximum wait time between database retries: 5 minutes
21
21
  MAX_RECONNECT_DELAY = 60 * 5
22
22
 
23
- attr_reader :state, :subscription, :consumer_tag
23
+ attr_reader :state, :subscription, :consumer_tag, :delayed
24
24
 
25
25
  #
26
26
  # Creates a fox, a RabbitMQ consumer.
@@ -30,10 +30,13 @@ module Warren
30
30
  # @param name [String] The name of the consumer
31
31
  # @param subscription [Warren::Subscription] Describes the queue to subscribe to
32
32
  # @param adaptor [#recovered?,#handle,#env] An adaptor to handle framework specifics
33
+ # @param subscribed_class [Warren::Subscriber::Base] The class to process received messages
34
+ # @param delayed [Warren::DelayExchange] The details handling delayed message broadcast
33
35
  #
34
- def initialize(name:, subscription:, adaptor:, subscribed_class:)
36
+ def initialize(name:, subscription:, adaptor:, subscribed_class:, delayed:)
35
37
  @consumer_tag = "#{adaptor.env}_#{name}_#{Process.pid}"
36
38
  @subscription = subscription
39
+ @delayed = delayed
37
40
  @logger = Warren::LogTagger.new(logger: adaptor.logger, tag: "#{FOX} #{@consumer_tag}")
38
41
  @adaptor = adaptor
39
42
  @subscribed_class = subscribed_class
@@ -52,6 +55,7 @@ module Warren
52
55
  def run!
53
56
  starting!
54
57
  subscription.activate! # Set up the queues
58
+ delayed.activate!
55
59
  running! # Transition to running state
56
60
  subscribe! # Subscribe to the queue
57
61
 
@@ -117,7 +121,7 @@ module Warren
117
121
  end
118
122
  end
119
123
 
120
- # Cancels the consumer and unregisters it
124
+ # Cancels the consumer and un-registers it
121
125
  def unsubscribe!
122
126
  info { 'Unsubscribing' }
123
127
  @consumer&.cancel
@@ -1,6 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Warren
4
+ # Namespace for framework adaptors.
5
+ #
6
+ # A FrameworkAdaptor should implement the following instance methods:
7
+ #
8
+ # == recovered? => Bool
9
+ # Indicates that any temporary issues (such as database connectivity problems)
10
+ # are resolved and consumers may restart.
11
+ #
12
+ # == handle
13
+ #
14
+ # Wraps the processing of each message, is expected to `yield` to allow
15
+ # processing. May be responsible for handling connection pools, and
16
+ # framework-specific exceptions. Raising {Warren::Exceptions::TemporaryIssue}
17
+ # here will cause consumers to sleep until `recovered?` returns true.
18
+ #
19
+ # == env => String
20
+ #
21
+ # Returns the current environment of the application.
22
+ #
23
+ # == logger => Logger
24
+ #
25
+ # Returns your application logger. Is expected to be compatible with the
26
+ # standard library Logger class.
27
+ # @see https://ruby-doc.org/stdlib-2.7.0/libdoc/logger/rdoc/Logger.html
28
+ #
29
+ # == load_application
30
+ #
31
+ # Called upon running `warren consumer start`. Should ensure your application
32
+ # is correctly loaded sufficiently for processing messages
33
+ #
4
34
  module FrameworkAdaptor
5
35
  # The RailsAdaptor provides error handling and application
6
36
  # loading for Rails applications
@@ -33,6 +63,11 @@ module Warren
33
63
  end
34
64
  end
35
65
 
66
+ #
67
+ # Checks that the database has recovered to allow message processing
68
+ #
69
+ # @return [Bool] Returns true if the application has recovered
70
+ #
36
71
  def recovered?
37
72
  ActiveRecord::Base.connection.reconnect!
38
73
  true
@@ -40,6 +75,14 @@ module Warren
40
75
  false
41
76
  end
42
77
 
78
+ #
79
+ # Checks ensures a database connection has been checked out before
80
+ # yielding to allow message processing. Rescues loss of the database
81
+ # connection and raises {Warren::Exceptions::TemporaryIssue} to send
82
+ # the consumers to sleep until it recovers.
83
+ #
84
+ # @return [Void]
85
+ #
43
86
  def handle
44
87
  with_connection do
45
88
  yield
@@ -60,14 +103,23 @@ module Warren
60
103
  ActiveRecord::Base.clear_active_connections!
61
104
  end
62
105
 
106
+ # Returns the rails environment
107
+ #
108
+ # @return [ActiveSupport::StringInquirer] The rails environment
63
109
  def env
64
110
  Rails.env
65
111
  end
66
112
 
113
+ # Returns the configured logger
114
+ #
115
+ # @return [Logger,ActiveSupport::Logger,...] The application logger
67
116
  def logger
68
117
  Rails.logger
69
118
  end
70
119
 
120
+ # Triggers full loading of the rails application and dependencies
121
+ #
122
+ # @return [Void]
71
123
  def load_application
72
124
  $stdout.puts 'Loading application...'
73
125
  require './config/environment'
@@ -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
@@ -17,25 +17,47 @@ module Warren
17
17
  class Channel
18
18
  extend Forwardable
19
19
 
20
+ attr_reader :routing_key_prefix
21
+
20
22
  def_delegators :@bun_channel, :close, :exchange, :queue, :prefetch, :ack, :nack
21
23
 
22
- def initialize(bun_channel, routing_key_template:, exchange: nil)
24
+ def initialize(bun_channel, routing_key_prefix:, exchange: nil)
23
25
  @bun_channel = bun_channel
24
26
  @exchange_name = exchange
25
- @routing_key_template = routing_key_template
27
+ @routing_key_prefix = routing_key_prefix
28
+ @routing_key_template = Handler.routing_key_template(routing_key_prefix)
26
29
  end
27
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
+ #
28
38
  def <<(message)
29
- default_exchange.publish(message.payload, routing_key: key_for(message))
39
+ publish(message)
40
+ end
41
+
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)
30
52
  self
31
53
  end
32
54
 
33
55
  private
34
56
 
35
- def default_exchange
57
+ def configured_exchange
36
58
  raise StandardError, 'No exchange configured' if @exchange_name.nil?
37
59
 
38
- @default_exchange ||= exchange(@exchange_name, auto_delete: false, durable: true, type: :topic)
60
+ @configured_exchange ||= exchange(@exchange_name, auto_delete: false, durable: true, type: :topic)
39
61
  end
40
62
 
41
63
  def key_for(message)
@@ -56,7 +78,7 @@ module Warren
56
78
  @server = server
57
79
  @exchange_name = exchange
58
80
  @pool_size = pool_size
59
- @routing_key_template = Handler.routing_key_template(routing_key_prefix)
81
+ @routing_key_prefix = routing_key_prefix
60
82
  end
61
83
 
62
84
  #
@@ -105,9 +127,9 @@ module Warren
105
127
  self
106
128
  end
107
129
 
108
- def new_channel
109
- Channel.new(session.create_channel(nil, 1), exchange: @exchange_name,
110
- routing_key_template: @routing_key_template)
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)
111
133
  end
112
134
 
113
135
  private
@@ -14,6 +14,13 @@ module Warren
14
14
  @routing_key_template = routing_key_template
15
15
  end
16
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
+ #
17
24
  def <<(message)
18
25
  @logger.info "Published: #{key_for(message)}"
19
26
  @logger.debug "Payload: #{message.payload}"
@@ -40,6 +47,7 @@ module Warren
40
47
  end
41
48
  end
42
49
 
50
+ # Small object to track exchange properties for logging purposes
43
51
  Exchange = Struct.new(:name, :options)
44
52
 
45
53
  # Queue class to provide extended logging in development mode
@@ -77,13 +77,16 @@ module Warren
77
77
  @warren = warren
78
78
  end
79
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
+ #
80
87
  def <<(message)
81
88
  @warren << message
82
89
  end
83
-
84
- def add_exchange(name, options)
85
- @warren.add_exchange(name, options)
86
- end
87
90
  end
88
91
 
89
92
  #
@@ -100,7 +103,7 @@ module Warren
100
103
  end
101
104
 
102
105
  #
103
- # Yields a new chanel, which proxies all message back to {messages} on the
106
+ # Yields a new channel, which proxies all message back to {messages} on the
104
107
  # {Warren::Handler::Test}
105
108
  #
106
109
  # @return [void]
@@ -111,7 +114,7 @@ module Warren
111
114
  end
112
115
 
113
116
  #
114
- # Returns a new chanel, which proxies all message back to {messages} on the
117
+ # Returns a new channel, which proxies all message back to {messages} on the
115
118
  # {Warren::Handler::Test}
116
119
  #
117
120
  # @return [Warren::Test::Channel] A rabbitMQ channel that logs messaged to the test warren
@@ -185,10 +188,6 @@ module Warren
185
188
  @messages << message if @enabled
186
189
  end
187
190
 
188
- def add_exchange(name, options)
189
- @exchanges << [name, options] if @enabled
190
- end
191
-
192
191
  private
193
192
 
194
193
  def raise_if_not_tracking
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative 'message/short'
4
4
  require_relative 'message/full'
5
+ require_relative 'message/simple'
5
6
 
6
7
  # Namespace to collect message formats
7
8
  # A Warren compatible message must implement:
8
9
  # routing_key: returns the routing_key for the message
9
10
  # payload: returns the message payload
11
+ # headers: Returns a headers hash
10
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
@@ -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
@@ -54,6 +54,14 @@ module Warren
54
54
  def payload
55
55
  [@class_name, @id].to_json
56
56
  end
57
+
58
+ #
59
+ # For compatibility. Returns an empty hash.
60
+ #
61
+ # @return [{}] Empty hash
62
+ def headers
63
+ {}
64
+ end
57
65
  end
58
66
  end
59
67
  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
@@ -24,7 +24,8 @@ module Warren
24
24
  # delegators (Supplied by Forwardable)
25
25
  # Essentially syntax is:
26
26
  # def_delegators <target>, *<methods_to_delegate>
27
- def_delegators :fox, :subscription, :warn, :info, :error, :debug
27
+ def_delegators :fox, :subscription, :warn, :info, :error, :debug, :delayed
28
+ def_delegators :delivery_info, :routing_key, :delivery_tag
28
29
 
29
30
  #
30
31
  # Construct a basic subscriber for each received message. Call {#process}
@@ -69,6 +70,8 @@ module Warren
69
70
  warn "Re-queue: #{payload}"
70
71
  warn "Re-queue Exception: #{exception.message}"
71
72
  raise_if_acknowledged
73
+ # nack arguments: delivery_tag, multiple, requeue
74
+ # http://reference.rubybunny.info/Bunny/Channel.html#nack-instance_method
72
75
  subscription.nack(delivery_tag, false, true)
73
76
  @acknowledged = true
74
77
  warn 'Re-queue nacked'
@@ -90,18 +93,43 @@ module Warren
90
93
  error 'Dead-letter nacked'
91
94
  end
92
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
+
93
117
  private
94
118
 
119
+ def max_retries
120
+ 30
121
+ end
122
+
123
+ def attempt
124
+ headers.fetch('attempts', 0)
125
+ end
126
+
95
127
  def headers
96
128
  # Annoyingly it appears that a message with no headers
97
129
  # returns nil, not an empty hash
98
130
  properties.headers || {}
99
131
  end
100
132
 
101
- def delivery_tag
102
- delivery_info.delivery_tag
103
- end
104
-
105
133
  # Acknowledge the message as successfully processed.
106
134
  # Will raise {Warren::MultipleAcknowledgements} if the message has been
107
135
  # acknowledged or rejected already.
@@ -11,14 +11,14 @@ module Warren
11
11
  # Great a new subscription. Handles queue creation, binding and attaching
12
12
  # consumers to the queues
13
13
  #
14
- # @param channel [Warren::Handler::Broadcast::Channel] A chanel on which to register queues
14
+ # @param channel [Warren::Handler::Broadcast::Channel] A channel on which to register queues
15
15
  # @param config [Hash] queue configuration hash
16
16
  #
17
17
  def initialize(channel:, config:)
18
18
  @channel = channel
19
- @queue_name = config.fetch('name')
20
- @queue_options = config.fetch('options')
21
- @bindings = config.fetch('bindings')
19
+ @queue_name = config&.fetch('name')
20
+ @queue_options = config&.fetch('options')
21
+ @bindings = config&.fetch('bindings')
22
22
  end
23
23
 
24
24
  def_delegators :channel, :nack, :ack
@@ -58,13 +58,20 @@ module Warren
58
58
  def queue
59
59
  raise StandardError, 'No queue configured' if @queue_name.nil?
60
60
 
61
- channel.queue(@queue_name, @queue_options)
61
+ @queue ||= channel.queue(@queue_name, @queue_options)
62
62
  end
63
63
 
64
64
  def establish_bindings!
65
65
  @bindings.each do |binding_config|
66
66
  exchange = exchange(binding_config['exchange'])
67
- add_binding(exchange, binding_config['options'])
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)
68
75
  end
69
76
  end
70
77
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Warren
4
4
  # Gem version number. Bump prior to release of new version
5
- VERSION = '0.2.0.rc1'
5
+ VERSION = '0.4.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sanger_warren
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.rc1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Glover
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-04 00:00:00.000000000 Z
11
+ date: 2021-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -80,6 +80,7 @@ files:
80
80
  - ".gitignore"
81
81
  - ".rspec"
82
82
  - ".rubocop.yml"
83
+ - ".yardopts"
83
84
  - CHANGELOG.md
84
85
  - Gemfile
85
86
  - Gemfile.lock
@@ -107,6 +108,7 @@ files:
107
108
  - lib/warren/callback/broadcast_with_warren.rb
108
109
  - lib/warren/client.rb
109
110
  - lib/warren/config/consumers.rb
111
+ - lib/warren/delay_exchange.rb
110
112
  - lib/warren/den.rb
111
113
  - lib/warren/exceptions.rb
112
114
  - lib/warren/fox.rb
@@ -121,6 +123,7 @@ files:
121
123
  - lib/warren/message.rb
122
124
  - lib/warren/message/full.rb
123
125
  - lib/warren/message/short.rb
126
+ - lib/warren/message/simple.rb
124
127
  - lib/warren/railtie.rb
125
128
  - lib/warren/subscriber/base.rb
126
129
  - lib/warren/subscription.rb
@@ -148,9 +151,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
151
  version: 2.6.0
149
152
  required_rubygems_version: !ruby/object:Gem::Requirement
150
153
  requirements:
151
- - - ">"
154
+ - - ">="
152
155
  - !ruby/object:Gem::Version
153
- version: 1.3.1
156
+ version: '0'
154
157
  requirements: []
155
158
  rubygems_version: 3.1.4
156
159
  signing_key: