sanger_warren 0.2.0 → 0.3.0.pre.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db1e22cb56e5b310539f2376823db23e6ffe3f711f12e685a525f63a32532c22
4
- data.tar.gz: e8ea84bdb2367d12ff0436a35e2681656f781b7ed2ab753fc75554760dcd315c
3
+ metadata.gz: 142ece2de33d995ad8eb33759edcefe6b265bd42ac71d0a13617a926a7cf0842
4
+ data.tar.gz: bdee8b37ee9b8b5dd8e3659f23ad129b1fb020d23c767458502eea5940672ac9
5
5
  SHA512:
6
- metadata.gz: 0fd88000c3f130678fbd6a0cd353c82b7062f874d796859c1243390fa669ad8d4534c949ce1d63ba3dab8397dbc235705ef0c0c3ac6c1f154c4a72ac7dba55b8
7
- data.tar.gz: 51f722402842324e71604693f028d109777807ddf0ae254ef48ac094f4b91fda71c98b9bb3a028f4b005c7ed9126b159517baf8e503bafdc9ea81cbf2cc1c443
6
+ metadata.gz: fb6414532226afc214ba4d79e757315fe177d50e6ac94516d5e891fc86321fdafd301c08134846e83edf7ebaf5885eab6b0d43f2fd149b1cc7ca18e72f3e8c61
7
+ data.tar.gz: 318bd13ea663117d972f6351fcbcb2cd2758e0059d3e19828a2bf70a90575bcf38fd393038878d5fd253a6224ee3d8f14597843a252d14087097081ad13aeba2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ Unreleased section to make new releases easy.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Added
9
+
10
+ - Added support for delay exchanges to process messages after a fixed delay
11
+ - Increased documentation
12
+ - Added Warren::Message::Simple for wrapping just routing key and payload.
13
+ - Added optional worker_count to warren_consumers.yml to control number of worker threads
14
+
15
+ ### Removed
16
+
17
+ - Warren::Handler::Test and Warren::Handler::Test::Channel no loner respond to
18
+ `add_exchange`. These methods were undocumented, and unused internally.
19
+
20
+ ## Changed
21
+
22
+ - Messages must now implement {#headers}, although simply returning an empty
23
+ hash is sufficient.
24
+ - Subscriber templates now use the path 'app/warren/subscriber' rather than
25
+ 'app/warren/subscribers' to correctly match class namespacing.
26
+ - 3 consumer worker threads will be spun up by default
27
+
8
28
  ## [0.2.0]
9
29
 
10
30
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sanger_warren (0.2.0)
4
+ sanger_warren (0.3.0.pre.rc1)
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)
@@ -30,16 +30,16 @@ GEM
30
30
  concurrent-ruby (1.1.8)
31
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)
@@ -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
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
@@ -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
+ # Great 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,16 @@
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
10
  # Currently its pretty simple, but in future will also handle registration of
10
11
  # delay and dead-letter queues/exchanges.
11
12
  class Den
13
+ # The number of simultaneous workers generated by default
14
+ DEFAULT_WORKER_COUNT = 3
15
+
12
16
  #
13
17
  # Create a {Warren::Fox} work pool.
14
18
  # @param app_name [String] The name of the application. Corresponds to the
@@ -34,9 +38,10 @@ module Warren
34
38
  config = dead_letter_config
35
39
  return unless config
36
40
 
37
- channel = Warren.handler.new_channel
38
- subscription = Warren::Subscription.new(channel: channel, config: config)
39
- subscription.activate!
41
+ Warren.handler.with_channel do |channel|
42
+ subscription = Warren::Subscription.new(channel: channel, config: config)
43
+ subscription.activate!
44
+ end
40
45
  end
41
46
 
42
47
  private
@@ -54,12 +59,18 @@ module Warren
54
59
  # and while we *can* share channels between consumers it results in them
55
60
  # sharing the same worker pool. This process lets us control workers on
56
61
  # a per-queue basis. Currently that just means one worker per consumer.
57
- channel = Warren.handler.new_channel
62
+ channel = Warren.handler.new_channel(worker_count: worker_count)
58
63
  subscription = Warren::Subscription.new(channel: channel, config: queue_config)
64
+ delay = Warren::DelayExchange.new(channel: channel, config: delay_config)
59
65
  Warren::Fox.new(name: @app_name,
60
66
  subscription: subscription,
61
67
  adaptor: @adaptor,
62
- subscribed_class: subscribed_class)
68
+ subscribed_class: subscribed_class,
69
+ delayed: delay)
70
+ end
71
+
72
+ def worker_count
73
+ consumer_config.fetch('worker_count', DEFAULT_WORKER_COUNT)
63
74
  end
64
75
 
65
76
  def queue_config
@@ -70,6 +81,10 @@ module Warren
70
81
  consumer_config.fetch('dead_letters')
71
82
  end
72
83
 
84
+ def delay_config
85
+ consumer_config.fetch('delay', nil)
86
+ end
87
+
73
88
  def subscribed_class
74
89
  Object.const_get(consumer_config.fetch('subscribed_class'))
75
90
  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,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Warren
4
+ # Namespace for framework adaptors.
5
+ # A FrameworkAdaptor should implement the following instance methods:
6
+ #
7
+ ## recovered? => Bool
8
+ # Indicates that any temporary issues (such as database connectivity problems)
9
+ # are resolved and consumers may restart.
10
+ #
11
+ ## handle
12
+ #
13
+ # Wraps the processing of each message, is expected to `yield` to allow
14
+ # processing. May be responsible for handling connection pools, and
15
+ # framework-specific exceptions. Raising {Warren::Exceptions::TemporaryIssue}
16
+ # here will cause consumers to sleep until `recovered?` returns true.
17
+ #
18
+ ## env => String
19
+ #
20
+ # Returns the current environment of the application.
21
+ #
22
+ ## logger => Logger
23
+ #
24
+ # Returns your application logger. Is expected to be compatible with the
25
+ # standard library Logger class.
26
+ # @see https://ruby-doc.org/stdlib-2.7.0/libdoc/logger/rdoc/Logger.html
27
+ #
28
+ ## load_application
29
+ #
30
+ # Called upon running `warren consumer start`. Should ensure your application
31
+ # is correctly loaded sufficiently for processing messages
32
+ #
4
33
  module FrameworkAdaptor
5
34
  # The RailsAdaptor provides error handling and application
6
35
  # loading for Rails applications
@@ -33,6 +62,11 @@ module Warren
33
62
  end
34
63
  end
35
64
 
65
+ #
66
+ # Checks that the database has recovered to allow message processing
67
+ #
68
+ # @return [Bool] Returns true if the application has recovered
69
+ #
36
70
  def recovered?
37
71
  ActiveRecord::Base.connection.reconnect!
38
72
  true
@@ -40,6 +74,14 @@ module Warren
40
74
  false
41
75
  end
42
76
 
77
+ #
78
+ # Checks ensures a database connection has been checked out before
79
+ # yielding to allow message processing. Rescues loss of the database
80
+ # connection and raises {Warren::Exceptions::TemporaryIssue} to send
81
+ # the consumers to sleep until it recovers.
82
+ #
83
+ # @return [Void]
84
+ #
43
85
  def handle
44
86
  with_connection do
45
87
  yield
@@ -60,14 +102,23 @@ module Warren
60
102
  ActiveRecord::Base.clear_active_connections!
61
103
  end
62
104
 
105
+ # Returns the rails environment
106
+ #
107
+ # @return [ActiveSupport::StringInquirer] The rails environment
63
108
  def env
64
109
  Rails.env
65
110
  end
66
111
 
112
+ # Returns the configured logger
113
+ #
114
+ # @return [Logger,ActiveSupport::Logger,...] The application logger
67
115
  def logger
68
116
  Rails.logger
69
117
  end
70
118
 
119
+ # Triggers full loading of the rails application and dependencies
120
+ #
121
+ # @return [Void]
71
122
  def load_application
72
123
  $stdout.puts 'Loading application...'
73
124
  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
@@ -28,17 +28,36 @@ module Warren
28
28
  @routing_key_template = Handler.routing_key_template(routing_key_prefix)
29
29
  end
30
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
+ #
31
38
  def <<(message)
32
- 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)
33
52
  self
34
53
  end
35
54
 
36
55
  private
37
56
 
38
- def default_exchange
57
+ def configured_exchange
39
58
  raise StandardError, 'No exchange configured' if @exchange_name.nil?
40
59
 
41
- @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)
42
61
  end
43
62
 
44
63
  def key_for(message)
@@ -108,9 +127,9 @@ module Warren
108
127
  self
109
128
  end
110
129
 
111
- def new_channel
112
- Channel.new(session.create_channel(nil, 1), exchange: @exchange_name,
113
- routing_key_prefix: @routing_key_prefix)
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)
114
133
  end
115
134
 
116
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
  #
@@ -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,9 @@
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
+ Simple = Struct.new(:routing_key, :payload, :headers)
8
+ end
9
+ 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.
@@ -16,9 +16,9 @@ module Warren
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,7 +58,7 @@ 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!
@@ -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'
5
+ VERSION = '0.3.0-rc1'
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
4
+ version: 0.3.0.pre.rc1
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-06 00:00:00.000000000 Z
11
+ date: 2021-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -107,6 +107,7 @@ files:
107
107
  - lib/warren/callback/broadcast_with_warren.rb
108
108
  - lib/warren/client.rb
109
109
  - lib/warren/config/consumers.rb
110
+ - lib/warren/delay_exchange.rb
110
111
  - lib/warren/den.rb
111
112
  - lib/warren/exceptions.rb
112
113
  - lib/warren/fox.rb
@@ -121,6 +122,7 @@ files:
121
122
  - lib/warren/message.rb
122
123
  - lib/warren/message/full.rb
123
124
  - lib/warren/message/short.rb
125
+ - lib/warren/message/simple.rb
124
126
  - lib/warren/railtie.rb
125
127
  - lib/warren/subscriber/base.rb
126
128
  - lib/warren/subscription.rb
@@ -148,9 +150,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
150
  version: 2.6.0
149
151
  required_rubygems_version: !ruby/object:Gem::Requirement
150
152
  requirements:
151
- - - ">="
153
+ - - ">"
152
154
  - !ruby/object:Gem::Version
153
- version: '0'
155
+ version: 1.3.1
154
156
  requirements: []
155
157
  rubygems_version: 3.1.4
156
158
  signing_key: