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
data/bin/console CHANGED
@@ -2,14 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'bundler/setup'
5
- require 'Warren'
5
+ require 'warren'
6
6
 
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
9
9
 
10
10
  # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require 'irb'
15
- IRB.start(__FILE__)
11
+ require 'pry'
12
+ Pry.start
data/bin/warren ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'warren/app/cli'
5
+
6
+ Warren::App::Cli.start(ARGV)
data/lefthook.yml ADDED
@@ -0,0 +1,53 @@
1
+ # https://github.com/Arkweid/lefthook
2
+ # Rubocop globs built based on https://github.com/rubocop-hq/rubocop/blob/master/config/default.yml With globbing support povided by
3
+ # https://pkg.go.dev/github.com/gobwas/glob#section-documentation
4
+ # Has the advantage of quicker commits when no rubocop files
5
+ pre-commit:
6
+ parallel: true
7
+ commands:
8
+ rubocop:
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 autocorrect on staged files only'; exit 1)
11
+
12
+ fix:
13
+ parallel: true
14
+ commands:
15
+ rubocop:
16
+ 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}'
17
+ run: rubocop --display-style-guide --extra-details --auto-correct --force-exclusion {staged_files}
18
+
19
+
20
+ # EXAMPLE USAGE
21
+ # Refer for explanation to following link:
22
+ # https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md
23
+ #
24
+ # pre-push:
25
+ # commands:
26
+ # packages-audit:
27
+ # tags: frontend security
28
+ # run: yarn audit
29
+ # gems-audit:
30
+ # tags: backend security
31
+ # run: bundle audit
32
+ #
33
+ # pre-commit:
34
+ # parallel: true
35
+ # commands:
36
+ # eslint:
37
+ # glob: "*.{js,ts}"
38
+ # run: yarn eslint {staged_files}
39
+ # rubocop:
40
+ # tags: backend style
41
+ # glob: "*.rb"
42
+ # exclude: "application.rb|routes.rb"
43
+ # run: bundle exec rubocop --force-exclusion {all_files}
44
+ # govet:
45
+ # tags: backend style
46
+ # files: git ls-files -m
47
+ # glob: "*.go"
48
+ # run: go vet {files}
49
+ # scripts:
50
+ # "hello.js":
51
+ # runner: node
52
+ # "any.go":
53
+ # runner: go run
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # While the gem is sanger_warren we actually namespace under Warren.
4
+ # The gem 'warren' performs a very similar function, but hasn't been updated
5
+ # for ten years.
6
+ # We need to include this file to ensure bundler automatically requires warren,
7
+ # thereby triggering the railties
8
+ require 'warren'
data/lib/warren.rb CHANGED
@@ -4,26 +4,71 @@ require 'warren/version'
4
4
  require 'warren/callback'
5
5
  require 'warren/handler'
6
6
  require 'warren/message'
7
+ require 'warren/subscriber/base'
8
+
9
+ # Load railties if rails is available
10
+ require 'warren/railtie' if defined?(Rails::Railtie)
7
11
 
8
12
  #
9
13
  # Module Warren provides connection pooling for RabbitMQ Connections
10
14
  #
11
15
  module Warren
16
+ # Environmental variables
17
+ WARREN_TYPE = 'WARREN_TYPE'
18
+
19
+ #
20
+ # Construct a {Warren::Handler::Base} of the type `type`.
21
+ # For Rails apps this is usually handled automatically by the initializer.
22
+ #
23
+ # @param type ['test','log','broadcast'] The type of warren handler to construct
24
+ # @param config [Hash] A configuration hash object
25
+ # @option config [Hash] :server Bunny connection parameters
26
+ # http://rubybunny.info/articles/connecting.html#using_a_map_of_parameters
27
+ # @option config [String] :exchange The default exchange to receive published messaged
28
+ # @option config [String] :routing_key_prefix A prefix to apply to all routing keys (Such as the environment)
29
+ #
30
+ # @return [Warren::Handler::Base] Exact class determined by the type passed in
31
+ #
12
32
  def self.construct(type:, config: {})
13
- case type
33
+ warren_type = ENV.fetch(WARREN_TYPE, type)
34
+ case warren_type
14
35
  when 'test' then Warren::Handler::Test.new
15
36
  when 'log' then Warren::Handler::Log.new(logger: config.fetch(:logger) { Rails.logger })
16
- when 'broadcast' then Warren::Handler::Broadcast.new(config)
17
- else raise StandardError, "Unknown type warren: #{type}"
37
+ when 'broadcast' then Warren::Handler::Broadcast.new(**config)
38
+ else raise StandardError, "Unknown type warren: #{warren_type}"
18
39
  end
19
40
  end
20
41
 
42
+ # Constructs a Warren::Handler of the specified type and sets it as the global handler.
21
43
  def self.setup(opts, logger: Rails.logger)
22
44
  logger.warn 'Recreating Warren handler when one already exists' if handler.present?
23
- @handler = construct(opts.symbolize_keys)
45
+ @handler = construct(**opts.symbolize_keys)
24
46
  end
25
47
 
48
+ #
49
+ # Returns the global Warren handler
50
+ #
51
+ # @return [Warren::Handler::Base] A warren handler for broadcasting messages
52
+ #
26
53
  def self.handler
27
54
  @handler
28
55
  end
56
+
57
+ # When we invoke the warren consumer, we end up loading warren before
58
+ # rails is loaded, so don't invoke the railtie, and don't get a change to do
59
+ # so until after the Rails has initialized, and thus run its ties.
60
+ # I'm sure there is a proper way of handling this, but want to move on for now.
61
+ def self.load_configuration
62
+ config = begin
63
+ Rails.application.config_for(:warren)
64
+ rescue RuntimeError => e
65
+ warn <<~WARN
66
+ 🐇 WARREN CONFIGURATION ERROR
67
+ #{e.message}
68
+ Use `warren config` to generate a basic configuration file
69
+ WARN
70
+ exit 1
71
+ end
72
+ Warren.setup(config.deep_symbolize_keys.slice(:type, :config))
73
+ end
29
74
  end
data/lib/warren/app.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warren
4
+ # The command line application can be triggered by `bundle exec warren`.
5
+ # It provides tools for configuring your warren set-up, as well as starting
6
+ # consumers.
7
+ module App
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'config'
5
+ require_relative 'consumer'
6
+
7
+ module Warren
8
+ module App
9
+ # Warren Thor CLI application used to:
10
+ # - Generate the configuration
11
+ # - Update the configuration with new consumers
12
+ # - Start and stop consumers
13
+ # @see http://whatisthor.com
14
+ class Cli < Thor
15
+ # Ensure we exit with an error in the event of failure
16
+ def self.exit_on_failure?
17
+ true
18
+ end
19
+
20
+ desc 'config', 'generate a basic warren config file'
21
+ option :path, type: :string,
22
+ default: 'config/warren.yml',
23
+ desc: 'The path to the configuration file to generate'
24
+ option :exchange, type: :string,
25
+ desc: 'The RabbitMQ exchange to connect to'
26
+ # Invoked by `$ warren config` generates a `warren.yml` file.
27
+ def config
28
+ Warren::App::Config.invoke(self, path: options['path'], exchange: options['exchange'])
29
+ end
30
+
31
+ desc 'consumer {add|start}', 'add and start queue consumers'
32
+ subcommand 'consumer', Consumer
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warren
4
+ module App
5
+ # Handles the initial creation of the configuration object
6
+ class Config
7
+ # We keep the template as plain text as it allows us to add comments
8
+ TEMPLATE = <<~TEMPLATE
9
+ # By default the development environment just logs the message and
10
+ # payload. If you wish to enable broadcasting in development mode,
11
+ # the easiest way to do so is to set the ENV WARREN_TYPE.
12
+ # For example
13
+ # `WARREN_TYPE=broadcast bundle exec rails s`
14
+ # This will override the setting in warren.yml
15
+ development:
16
+ type: log
17
+ # Log mode does not actually use this configuration, but
18
+ # it is provided for convenience when broadcast mode is enabled.
19
+ # The provided settings are the default options of RabbitMQ
20
+ # DO NOT commit sensitive information in this file. Instead you may
21
+ # use the WARREN_CONNECTION_URI environmental variable
22
+ config:
23
+ server:
24
+ host: localhost
25
+ port: 5672
26
+ username: guest
27
+ password: guest
28
+ vhost: %<vhost>s
29
+ frame_max: 0
30
+ heartbeat: 30
31
+ exchange: %<exchange>s
32
+ routing_key_prefix: development
33
+ # The test environment sets up a test message handler, which lets
34
+ # you make assertions about which messages have been sent.
35
+ # See: https://rubydoc.info/gems/sanger_warren/Warren/Handler/Test
36
+ test:
37
+ type: test
38
+ config:
39
+ routing_key_prefix: test
40
+ # You are encouraged to use the WARREN_CONNECTION_URI environmental
41
+ # variable to configure your production environment. Under no
42
+ # circumstances should you commit sensitive information in the file.
43
+ TEMPLATE
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
+ #
55
+ def self.invoke(shell, path:, exchange: nil)
56
+ new(shell, path: path, exchange: exchange).invoke
57
+ end
58
+
59
+ #
60
+ # Generates a new warren.yml file at {#path}. Usually invoked via the
61
+ # `warren config` cli command
62
+ #
63
+ # @param shell [Thor::Shell::Basic] Thor shell instance for user interaction
64
+ # @param path [String] The path to the warren.yml file.
65
+ # @param exchange [String] The exchange to connect to
66
+ #
67
+ def initialize(shell, path:, exchange: nil)
68
+ @shell = shell
69
+ @path = path
70
+ @exchange = exchange
71
+ end
72
+
73
+ #
74
+ # Create a new configuration yaml file at {#path} using sensible defaults
75
+ # and the provided {#exchange}. If {#exchange} is nil, prompts the user
76
+ #
77
+ # @return [Void]
78
+ #
79
+ def invoke
80
+ return unless check_file?
81
+
82
+ @exchange ||= ask_exchange # Update our exchange before we do anything
83
+ File.open(@path, 'w') do |file|
84
+ file.write payload
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # The path to the config file
91
+ attr_reader :path
92
+ # The exchange to connect to
93
+ attr_reader :exchange
94
+
95
+ def payload
96
+ format(TEMPLATE, exchange: @exchange, vhost: '/')
97
+ end
98
+
99
+ def check_file?
100
+ return true unless File.exist?(@path)
101
+
102
+ @shell.yes? "#{@path} exists. Overwrite (Y/N)? "
103
+ end
104
+
105
+ def ask_exchange
106
+ @shell.ask 'Specify an exchange: '
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'consumer_add'
5
+ require_relative 'consumer_start'
6
+ require 'warren/config/consumers'
7
+
8
+ module Warren
9
+ module App
10
+ # Warren Thor CLI subcommand used to:
11
+ # - Add new consumer configurations
12
+ # - Start consumers
13
+ # @see http://whatisthor.com
14
+ class Consumer < Thor
15
+ include Thor::Actions
16
+
17
+ source_root("#{File.dirname(__FILE__)}/templates")
18
+
19
+ # Ensure we exit with an error in the event of failure
20
+ def self.exit_on_failure?
21
+ true
22
+ end
23
+
24
+ desc 'add NAME', 'generate a new warren consumer'
25
+ option :desc, type: :string,
26
+ desc: 'Brief description of consumer'
27
+ option :queue, type: :string,
28
+ desc: 'The RabbitMQ queue to create / connect to'
29
+ option :bindings, type: :array,
30
+ desc: 'bindings between the queue and exchange',
31
+ banner: '{direct|fanout|topic|headers}:EXCHANGE[:ROUTING_KEY_A[,ROUTING_KEY_B]]'
32
+ option :path, type: :string,
33
+ default: Warren::Config::Consumers::DEFAULT_PATH,
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
+ #
43
+ def add(name = nil)
44
+ say 'Adding a consumer'
45
+ Warren::App::ConsumerAdd.invoke(self, name, options)
46
+ end
47
+
48
+ desc 'start', 'start registered consumers'
49
+ option :path, type: :string,
50
+ default: Warren::Config::Consumers::DEFAULT_PATH,
51
+ desc: 'The path to the consumer configuration file to use'
52
+ option :consumers, type: :array,
53
+ desc: 'The consumers to start. Defaults to all consumers',
54
+ banner: 'consumer_name other_consumer'
55
+ # Invoked by `$ warren consumer start`. Starts up the configured consumers
56
+ #
57
+ # @return [Void]
58
+ #
59
+ def start
60
+ say 'Starting consumers'
61
+ Warren::App::ConsumerStart.invoke(self, options)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'exchange_config'
4
+ require 'warren/config/consumers'
5
+
6
+ module Warren
7
+ module App
8
+ # Handles the initial creation of the configuration object
9
+ class ConsumerAdd
10
+ # Default namespace for new Subscribers
11
+ SUBSCRIBER_NAMESPACE = %w[Warren Subscriber].freeze
12
+
13
+ attr_reader :name, :desc, :queue
14
+
15
+ #
16
+ # Add a consumer to the configuration file located at `options.path`
17
+ # Will prompt the user for input on the `shell` if information not
18
+ # provided upfront
19
+ #
20
+ # @param shell [Thor::Shell::Basic] Thor shell instance for feedback
21
+ # @param name [String] The name of the consumer
22
+ # @param options [Hash] Hash of command line arguments from Thor
23
+ # @option options [String] :desc Short description of consumer (for documentation)
24
+ # @option options [String] :queue Then name of the queue to bind to
25
+ # @option options [Array<String>] :bindings Array of binding in the format
26
+ # '<exchange_type>:<exchange_name>:<outing_key>,<routing_key>'
27
+ #
28
+ # @return [ConsumerAdd] The ConsumerAdd
29
+ #
30
+ def self.invoke(shell, name, options)
31
+ new(shell, name, options).invoke
32
+ end
33
+
34
+ # Create a consumer configuration object. Use {#invoke} to gather information and
35
+ # generate the config
36
+ #
37
+ # @param shell [Thor::Shell::Basic] Thor shell instance for feedback
38
+ # @param name [String] The name of the consumer
39
+ # @param options [Hash] Hash of command line arguments from Thor
40
+ # @option options [String] :desc Short description of consumer (for documentation)
41
+ # @option options [String] :queue Then name of the queue to bind to
42
+ # @option options [Array<String>] :bindings Array of binding in the format
43
+ # '<exchange_type>:<exchange_name>:<outing_key>,<routing_key>'
44
+ #
45
+ def initialize(shell, name, options)
46
+ @shell = shell
47
+ @name = name
48
+ @desc = options[:desc]
49
+ @queue = options[:queue]
50
+ @delay = options[:delay]
51
+ @config = Warren::Config::Consumers.new(options[:path])
52
+ @bindings = Warren::App::ExchangeConfig.parse(shell, options[:bindings])
53
+ end
54
+
55
+ #
56
+ # Create a new configuration yaml file at `@path` using sensible defaults
57
+ # and the provided exchange. If exchange is nil, prompts the user
58
+ #
59
+ # @return [Void]
60
+ #
61
+ def invoke
62
+ check_name if @name # Check name before we gather facts, as its better to know we
63
+ # might have an issue early.
64
+ gather_facts
65
+ write_configuration
66
+ write_subscriber
67
+ end
68
+
69
+ private
70
+
71
+ def subscribed_class
72
+ class_name = name.split(/[\s\-_]/).map(&:capitalize).join
73
+
74
+ [*SUBSCRIBER_NAMESPACE, class_name].join('::')
75
+ end
76
+
77
+ def check_name
78
+ while @config.consumer_exist?(@name)
79
+ @name = @shell.ask(
80
+ "Consumer named '#{@name}' already exists. Specify a alternative " \
81
+ 'consumer name: '
82
+ )
83
+ end
84
+ end
85
+
86
+ #
87
+ # Loads the configuration, should be a hash
88
+ #
89
+ # @return [Hash] A hash of consumer configurations indexed by name
90
+ #
91
+ def load_config
92
+ YAML.load_file(@path)
93
+ rescue Errno::ENOENT
94
+ {}
95
+ end
96
+
97
+ def gather_facts
98
+ @name ||= @shell.ask 'Specify a consumer name: '
99
+ check_name
100
+ @desc ||= @shell.ask 'Provide an optional description: '
101
+ @queue ||= @shell.ask 'Provide the name of the queue to connect to: '
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
106
+ nil
107
+ end
108
+
109
+ def gather_bindings
110
+ Warren::App::ExchangeConfig.ask(@shell)
111
+ end
112
+
113
+ def write_configuration
114
+ @config.add_consumer(
115
+ @name, desc: @desc, queue: @queue,
116
+ bindings: @bindings, subscribed_class: subscribed_class,
117
+ delay: @delay
118
+ )
119
+ @config.save
120
+ end
121
+
122
+ def write_subscriber
123
+ @shell.template('subscriber.tt', subscriber_path, context: binding)
124
+ end
125
+
126
+ def subscriber_path
127
+ "#{['app', *SUBSCRIBER_NAMESPACE, @name.tr(' -', '_')].map(&:downcase).join('/')}.rb"
128
+ end
129
+ end
130
+ end
131
+ end