harmoniser 0.9.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd51302d5c886af771b66e4528054042a8922071669719ba625aac5d5c0dc503
4
- data.tar.gz: 2dd00c5d6699436fca25d6c721cba80518aa8baa967e3bca247af408b829582b
3
+ metadata.gz: a1a7c351ba8ed80091b6e32dbd3cb0486f6b4123b30de12f1406862f3bdedcbb
4
+ data.tar.gz: f3d68be4e8cc7cb83e5df89bbc20ce4bc905b44711d59a23de50e7ac761bc5ae
5
5
  SHA512:
6
- metadata.gz: 6f2f43e8bc13d89db74bc38da9e08b86773aea3582cf5df277c985df22a016aa22bc5f965243b3df451c9f22ff40bc16299aeece5b7d5d15d57b6a43fb8b2233
7
- data.tar.gz: e3768b89e31de5b74b27333a990247b416067acc6055e4a1ea15d3ad63560a91780d9f379ced069f0cc9c950907186d2e67cd5daaacfaaf4b4f64b8715804415
6
+ metadata.gz: 5c6da5317a0ea480766ce716b7710598d6a1720afd8bb24cf82f2aecd3b96e9d41b31b3246d306c525e5ddbb5532f8adb87c7b3188a402ecada8c87a192176fb
7
+ data.tar.gz: 9a31ed8616f30072245dbc783692cadf871c46f2082f627420fd71a38f6e29b3d9162b6152fdf741173a67e4a16fb5d1c8fbe812eaffb081356f4b83796ad510
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0] - 2024-10-09
4
+
5
+ ### Added
6
+ - Attach callbacks for block/unblock at Connection
7
+ - Introduce Connectable to accommodate Session/Channel management
8
+ - Attach at_exit hook for maybe closing Publisher connection. Only applicable for cases in which harmoniser is not the process running.
9
+
10
+ ### Changed
11
+ - Separate connection for Publisher, Subscriber and Topology as per recommendations from [RabbitMQ official docs](https://www.rabbitmq.com/docs/alarms#effects-on-clusters)
12
+
13
+ ## [0.10.0] - 2024-09-16
14
+
15
+ ### Added
16
+ - Add a concurrency option to the CLI. By default, concurrency is unbounded, i.e., each subscriber
17
+ has its own thread dedicated to processing messages
18
+ - Introduce Harmoniser::Subscriber::Registry for holding references to classes that include
19
+ Harmoniser::Subscriber
20
+ - Kill the process when an ACK timeout is received through any channel. The process terminates with
21
+ exit code 138
22
+ - Add a 25-second timeout to shut down the process. This is only applicable to processes using the
23
+ concurrency option
24
+
25
+ ### Changed
26
+ - Update Bunny to the latest version, i.e., 2.23
27
+ - Cancel subscribers and connection is done within the Launcher context instead of relying on
28
+ at_exit hook
29
+ - Prevent the connection from being closed after Topology#declare finishes; only the channel used
30
+ is closed
31
+
3
32
  ## [0.9.0] - 2024-08-09
4
33
 
5
34
  ### Added
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 TODO: Write your name
3
+ Copyright (c) 2021 Jose Lloret Perez
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -111,12 +111,6 @@ To contribute to this codebase, you will need to setup your local development us
111
111
 
112
112
  You can also access the running container by executing `$ make shell` and then execute any commands related to Harmoniser within its isolated environment.
113
113
 
114
- ## Future Improvements
115
-
116
- - [ ] Feature: Introduce capability of configuring concurrency for Harmoniser process.
117
- - [ ] Issue: Reopen Channels anytime an exception occurs that closes them automatically. More info can be found [here](https://www.rabbitmq.com/channels.html#error-handling).
118
- - [ ] Chore: Introduce simplecov gem for code coverage.
119
-
120
114
  ## License
121
115
 
122
116
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/harmoniser.gemspec CHANGED
@@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.executables = ["harmoniser"]
25
25
  spec.require_paths = ["lib"]
26
26
 
27
- spec.add_runtime_dependency "bunny", "~> 2.22"
27
+ spec.add_runtime_dependency "bunny", "~> 2.23"
28
28
  end
@@ -5,11 +5,13 @@ require "harmoniser/launcher"
5
5
 
6
6
  module Harmoniser
7
7
  class CLI
8
+ class SigUsr1 < StandardError; end
8
9
  include Singleton
9
10
 
10
11
  SIGNAL_HANDLERS = {
11
12
  "INT" => lambda { |cli, signal| raise Interrupt },
12
- "TERM" => lambda { |cli, signal| raise Interrupt }
13
+ "TERM" => lambda { |cli, signal| raise Interrupt },
14
+ "USR1" => lambda { |cli, signal| raise SigUsr1 }
13
15
  }
14
16
  SIGNAL_HANDLERS.default = lambda { |cli, signal| cli.logger.info("Default signal handler executed since there is no handler defined: signal = `#{signal}`") }
15
17
 
@@ -22,6 +24,7 @@ module Harmoniser
22
24
 
23
25
  def call
24
26
  parse_options
27
+ @launcher = Launcher.call(configuration: @configuration, logger: @logger)
25
28
  run
26
29
  end
27
30
 
@@ -37,7 +40,7 @@ module Harmoniser
37
40
  def define_signals
38
41
  @read_io, @write_io = IO.pipe
39
42
 
40
- ["INT", "TERM"].each do |sig|
43
+ ["INT", "TERM", "USR1"].each do |sig|
41
44
  Signal.trap(sig) do
42
45
  @write_io.puts(sig)
43
46
  end
@@ -45,9 +48,7 @@ module Harmoniser
45
48
  end
46
49
 
47
50
  def run
48
- Launcher
49
- .new(configuration: configuration, logger: logger)
50
- .start
51
+ @launcher.start
51
52
 
52
53
  define_signals
53
54
 
@@ -58,7 +59,13 @@ module Harmoniser
58
59
  rescue Interrupt
59
60
  @write_io.close
60
61
  @read_io.close
62
+ @launcher.stop
61
63
  exit(0)
64
+ rescue SigUsr1
65
+ @write_io.close
66
+ @read_io.close
67
+ @launcher.stop
68
+ exit(128 + 10)
62
69
  end
63
70
 
64
71
  def handle_signal(signal)
@@ -1,10 +1,7 @@
1
- require "forwardable"
2
1
  require "harmoniser/configuration"
3
2
 
4
3
  module Harmoniser
5
4
  module Configurable
6
- extend Forwardable
7
-
8
5
  def configure
9
6
  @configuration ||= Configuration.new
10
7
  yield(@configuration)
@@ -19,7 +16,5 @@ module Harmoniser
19
16
  def default_configuration
20
17
  @configuration ||= Configuration.new
21
18
  end
22
-
23
- def_delegators :configuration, :connection, :connection?
24
19
  end
25
20
  end
@@ -1,15 +1,14 @@
1
1
  require "forwardable"
2
- require "harmoniser/connectable"
2
+ require "harmoniser/connection"
3
3
  require "harmoniser/topology"
4
4
  require "harmoniser/options"
5
5
 
6
6
  module Harmoniser
7
7
  class Configuration
8
8
  extend Forwardable
9
- include Connectable
10
9
 
11
10
  attr_reader :logger, :options
12
- def_delegators :options, :environment, :require, :verbose
11
+ def_delegators :options, :concurrency, :environment, :require, :verbose, :timeout
13
12
 
14
13
  def initialize
15
14
  @logger = Harmoniser.logger
@@ -18,6 +17,16 @@ module Harmoniser
18
17
  @topology = Topology.new
19
18
  end
20
19
 
20
+ def connection_opts
21
+ @connection_opts ||= Connection::DEFAULT_CONNECTION_OPTS
22
+ end
23
+
24
+ def connection_opts=(opts)
25
+ raise TypeError, "opts must be a Hash object" unless opts.is_a?(Hash)
26
+
27
+ @connection_opts = connection_opts.merge(opts)
28
+ end
29
+
21
30
  def define_topology
22
31
  raise LocalJumpError, "A block is required for this method" unless block_given?
23
32
 
@@ -33,8 +42,10 @@ module Harmoniser
33
42
 
34
43
  def default_options
35
44
  {
45
+ concurrency: Float::INFINITY,
36
46
  environment: ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "production")),
37
47
  require: ".",
48
+ timeout: 25,
38
49
  verbose: false
39
50
  }
40
51
  end
@@ -4,45 +4,70 @@ module Harmoniser
4
4
  module Connectable
5
5
  MUTEX = Mutex.new
6
6
 
7
- def connection_opts
8
- @connection_opts ||= Connection::DEFAULT_CONNECTION_OPTS.merge({logger: Harmoniser.logger})
9
- end
7
+ module ClassMethods
8
+ def connection(configuration = Harmoniser.configuration)
9
+ MUTEX.synchronize do
10
+ @connection ||= Connection.new(configuration.connection_opts)
11
+ @connection.start unless @connection.open? || @connection.recovering_from_network_failure?
12
+ @connection
13
+ end
14
+ end
10
15
 
11
- def connection_opts=(opts)
12
- raise TypeError, "opts must be a Hash object" unless opts.is_a?(Hash)
16
+ def connection?
17
+ !!defined?(@connection)
18
+ end
13
19
 
14
- @connection_opts = connection_opts.merge(opts)
15
- end
20
+ def create_channel(consumer_pool_size: 1, consumer_pool_shutdown_timeout: 60)
21
+ connection
22
+ .create_channel(nil, consumer_pool_size, false, consumer_pool_shutdown_timeout)
23
+ .tap do |channel|
24
+ attach_callbacks(channel)
25
+ end
26
+ end
16
27
 
17
- def connection
18
- MUTEX.synchronize do
19
- @connection ||= create_connection
20
- @connection.start unless @connection.open? || @connection.recovering_from_network_failure?
21
- @connection
28
+ private
29
+
30
+ def attach_callbacks(channel)
31
+ channel.on_error(&method(:on_error).to_proc)
32
+ channel.on_uncaught_exception(&method(:on_uncaught_exception).to_proc)
22
33
  end
23
- end
24
34
 
25
- def connection?
26
- !!defined?(@connection)
27
- end
35
+ def on_error(channel, amq_method)
36
+ attributes = {
37
+ amq_method: amq_method,
38
+ exchanges: channel.exchanges.keys,
39
+ queues: channel.consumers.values.map(&:queue)
40
+ }
28
41
 
29
- private
42
+ if amq_method.is_a?(AMQ::Protocol::Channel::Close)
43
+ attributes[:reply_code] = amq_method.reply_code
44
+ attributes[:reply_text] = amq_method.reply_text
45
+ end
30
46
 
31
- def create_connection
32
- at_exit(&method(:at_exit_handler).to_proc)
33
- Connection.new(connection_opts)
34
- end
47
+ stringified_attributes = attributes.map { |k, v| "#{k} = `#{v}`" }.join(", ")
48
+ Harmoniser.logger.error("Default on_error handler executed for channel: #{stringified_attributes}")
49
+ maybe_kill_process(amq_method)
50
+ end
51
+
52
+ def on_uncaught_exception(error, consumer)
53
+ Harmoniser.logger.error("Default on_uncaught_exception handler executed for channel: error_class = `#{error.class}`, error_message = `#{error.message}`, error_backtrace = `#{error.backtrace&.first(5)}, queue = `#{consumer.queue}`")
54
+ end
55
+
56
+ def maybe_kill_process(amq_method)
57
+ Process.kill("USR1", Process.pid) if ack_timed_out?(amq_method) && Harmoniser.server?
58
+ end
59
+
60
+ def ack_timed_out?(amq_method)
61
+ return false unless amq_method.is_a?(AMQ::Protocol::Channel::Close)
35
62
 
36
- def at_exit_handler
37
- logger = Harmoniser.logger
63
+ amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
64
+ end
65
+ end
38
66
 
39
- logger.info("Shutting down!")
40
- if connection? && @connection.open?
41
- logger.info("Connection will be closed: connection = `#{@connection}`")
42
- @connection.close
43
- logger.info("Connection closed: connection = `#{@connection}`")
67
+ class << self
68
+ def included(base)
69
+ base.extend(ClassMethods)
44
70
  end
45
- logger.info("Bye!")
46
71
  end
47
72
  end
48
73
  end
@@ -7,19 +7,11 @@ module Harmoniser
7
7
 
8
8
  DEFAULT_CONNECTION_OPTS = {
9
9
  connection_name: "harmoniser@#{VERSION}",
10
- connection_timout: 5,
10
+ connection_timeout: 5,
11
11
  host: "127.0.0.1",
12
12
  password: "guest",
13
13
  port: 5672,
14
14
  read_timeout: 5,
15
- recovery_attempt_started: proc {
16
- stringified_connection = Harmoniser.connection.to_s
17
- Harmoniser.logger.info("Recovery attempt started: connection = `#{stringified_connection}`")
18
- },
19
- recovery_completed: proc {
20
- stringified_connection = Harmoniser.connection.to_s
21
- Harmoniser.logger.info("Recovery completed: connection = `#{stringified_connection}`")
22
- },
23
15
  tls_silence_warnings: true,
24
16
  username: "guest",
25
17
  verify_peer: false,
@@ -29,20 +21,24 @@ module Harmoniser
29
21
 
30
22
  def_delegators :@bunny, :create_channel, :open?, :recovering_from_network_failure?
31
23
 
32
- def initialize(opts)
33
- @bunny = Bunny.new(opts)
24
+ def initialize(opts, logger: Harmoniser.logger)
25
+ @logger = logger
26
+ @bunny = Bunny.new(maybe_dynamic_opts(opts)).tap do |bunny|
27
+ attach_callbacks(bunny)
28
+ end
34
29
  end
35
30
 
36
31
  def to_s
37
- "<#{self.class.name}>: #{user}@#{host}:#{port}, connection_name = `#{connection_name}`, vhost = `#{vhost}`"
32
+ "<#{self.class.name}>:#{object_id} #{user}@#{host}:#{port}, connection_name = `#{connection_name}`, vhost = `#{vhost}`"
38
33
  end
39
34
 
35
+ # TODO Only perform retries when Harmoniser.server?
40
36
  def start
41
37
  retries = 0
42
38
  begin
43
39
  with_signal_handler { @bunny.start }
44
40
  rescue => e
45
- Harmoniser.logger.error("Connection attempt failed: retries = `#{retries}`, error_class = `#{e.class}`, error_message = `#{e.message}`")
41
+ @logger.error("Connection attempt failed: retries = `#{retries}`, error_class = `#{e.class}`, error_message = `#{e.message}`")
46
42
  with_signal_handler { sleep(1) }
47
43
  retries += 1
48
44
  retry
@@ -50,15 +46,26 @@ module Harmoniser
50
46
  end
51
47
 
52
48
  def close
53
- @bunny.close
54
- true
49
+ @logger.info("Connection will be closed: connection = `#{self}`")
50
+ @bunny.close.tap do
51
+ @logger.info("Connection closed: connection = `#{self}`")
52
+ end
55
53
  rescue => e
56
- Harmoniser.logger.error("Connection#close failed: error_class = `#{e.class}`, error_message = `#{e.message}`")
54
+ @logger.error("Connection#close failed: error_class = `#{e.class}`, error_message = `#{e.message}`")
57
55
  false
58
56
  end
59
57
 
60
58
  private
61
59
 
60
+ def attach_callbacks(bunny)
61
+ bunny.on_blocked do |blocked|
62
+ @logger.warn("Connection blocked: connection = `#{self}`, reason = `#{blocked.reason}`")
63
+ end
64
+ bunny.on_unblocked do |unblocked|
65
+ @logger.info("Connection unblocked: connection = `#{self}`")
66
+ end
67
+ end
68
+
62
69
  def connection_name
63
70
  @bunny.connection_name
64
71
  end
@@ -67,6 +74,18 @@ module Harmoniser
67
74
  @bunny.transport.host
68
75
  end
69
76
 
77
+ def maybe_dynamic_opts(opts)
78
+ opts.merge({
79
+ logger: opts.fetch(:logger) { @logger },
80
+ recovery_attempt_started: opts.fetch(:recovery_attempt_started) do
81
+ proc { @logger.info("Recovery attempt started: connection = `#{self}`") }
82
+ end,
83
+ recovery_completed: opts.fetch(:recovery_completed) do
84
+ proc { @logger.info("Recovery completed: connection = `#{self}`") }
85
+ end
86
+ })
87
+ end
88
+
70
89
  def port
71
90
  @bunny.transport.port
72
91
  end
@@ -79,10 +98,11 @@ module Harmoniser
79
98
  @bunny.vhost
80
99
  end
81
100
 
101
+ # TODO Use Signal handler defined at Harmoniser::CLI instead of rescuing SignalException?
82
102
  def with_signal_handler
83
103
  yield if block_given?
84
104
  rescue SignalException => e
85
- Harmoniser.logger.info("Signal received: signal = `#{Signal.signame(e.signo)}`")
105
+ @logger.info("Signal received: signal = `#{Signal.signame(e.signo)}`")
86
106
  Harmoniser.server? ? exit(0) : raise
87
107
  end
88
108
  end
@@ -0,0 +1,80 @@
1
+ require "harmoniser/subscriber"
2
+ require "harmoniser/subscriber/registry"
3
+ require "harmoniser/work_pool_reporter"
4
+
5
+ module Harmoniser
6
+ module Launcher
7
+ class Base
8
+ attr_reader :subscribers
9
+
10
+ def initialize(configuration:, logger:)
11
+ @configuration = configuration
12
+ @logger = logger
13
+ @subscribers = Subscriber.registry
14
+ end
15
+
16
+ def start
17
+ boot_app
18
+ start_subscribers
19
+ @logger.info("Subscribers registered to consume messages from queues: klasses = `#{@subscribers}`")
20
+ end
21
+
22
+ def stop
23
+ @logger.info("Shutting down!")
24
+ maybe_close
25
+ @logger.info("Bye!")
26
+ end
27
+
28
+ private
29
+
30
+ def boot_app
31
+ if File.directory?(@configuration.require)
32
+ load_rails
33
+ else
34
+ load_file
35
+ end
36
+ end
37
+
38
+ def load_rails
39
+ filepath = File.expand_path("#{@configuration.require}/config/environment.rb")
40
+ require filepath
41
+ rescue LoadError => e
42
+ @logger.warn("Error while requiring file within directory. No subscribers will run for this process: require = `#{@configuration.require}`, filepath = `#{filepath}`, error_class = `#{e.class}`, error_message = `#{e.message}`, error_backtrace = `#{e.backtrace&.first(5)}`")
43
+ end
44
+
45
+ def load_file
46
+ require @configuration.require
47
+ rescue LoadError => e
48
+ @logger.warn("Error while requiring file. No subscribers will run for this process: require = `#{@configuration.require}`, error_class = `#{e.class}`, error_message = `#{e.message}`, error_backtrace = `#{e.backtrace&.first(5)}`")
49
+ end
50
+
51
+ def maybe_close
52
+ maybe_close_subscriber
53
+ maybe_close_publisher
54
+ end
55
+
56
+ def maybe_close_publisher
57
+ return unless Publisher.connection?
58
+ Publisher.connection.close
59
+ end
60
+
61
+ def maybe_close_subscriber
62
+ return unless Subscriber.connection?
63
+
64
+ maybe_cancel_subscribers
65
+ report_work_pool
66
+ Subscriber.connection.close
67
+ end
68
+
69
+ def maybe_cancel_subscribers
70
+ @logger.info("Subscribers will be cancelled from queues: klasses = `#{@subscribers}`")
71
+ @subscribers.each(&:harmoniser_subscriber_stop)
72
+ @logger.info("Subscribers cancelled: klasses = `#{@subscribers}`")
73
+ end
74
+
75
+ def report_work_pool
76
+ @logger.info("Stats about the work pool: work_pool_reporter = `#{WorkPoolReporter.new(consumers: @consumers)}`. Note: A backlog greater than zero means messages could be lost for subscribers configured with no_ack, i.e. automatic ack")
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "base"
2
+
3
+ module Harmoniser
4
+ module Launcher
5
+ class Bounded < Base
6
+ private
7
+
8
+ def start_subscribers
9
+ @consumers = subscribers.map do |klass|
10
+ klass.harmoniser_subscriber_start(channel: channel)
11
+ end
12
+ end
13
+
14
+ def channel
15
+ @channel ||= Subscriber.create_channel(consumer_pool_size: @configuration.concurrency, consumer_pool_shutdown_timeout: @configuration.timeout)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "base"
2
+
3
+ module Harmoniser
4
+ module Launcher
5
+ class UnBounded < Base
6
+ def start_subscribers
7
+ @consumers = subscribers.map(&:harmoniser_subscriber_start)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,47 +1,14 @@
1
- module Harmoniser
2
- class Launcher
3
- def initialize(configuration:, logger:)
4
- @configuration = configuration
5
- @logger = logger
6
- end
7
-
8
- def start
9
- boot_app
10
- start_subscribers
11
- end
1
+ require_relative "launcher/bounded"
2
+ require_relative "launcher/unbounded"
12
3
 
13
- private
14
-
15
- def boot_app
16
- if File.directory?(@configuration.require)
17
- load_rails
18
- else
19
- load_file
20
- end
21
- end
4
+ module Harmoniser
5
+ module Launcher
6
+ class << self
7
+ def call(configuration:, logger:)
8
+ return Bounded.new(configuration:, logger:) unless configuration.options.unbounded_concurrency?
22
9
 
23
- # TODO - Frameworks like Rails which have autoload for development/test will not start any subscriber unless the files where subscribers are located are required explicitly. Since we premier production and the eager load ensures that every file is loaded, this approach works
24
- def start_subscribers
25
- klasses = Subscriber.harmoniser_included
26
- klasses.each do |klass|
27
- klass.harmoniser_subscriber_start
10
+ UnBounded.new(configuration:, logger:)
28
11
  end
29
- @logger.info("Subscribers registered to consume messages from queues: klasses = `#{klasses}`")
30
- end
31
-
32
- private
33
-
34
- def load_rails
35
- filepath = File.expand_path("#{@configuration.require}/config/environment.rb")
36
- require filepath
37
- rescue LoadError => e
38
- @logger.warn("Error while requiring file within directory. No subscribers will run for this process: require = `#{@configuration.require}`, filepath = `#{filepath}`, error_class = `#{e.class}`, error_message = `#{e.message}`, error_backtrace = `#{e.backtrace&.first(5)}`")
39
- end
40
-
41
- def load_file
42
- require @configuration.require
43
- rescue LoadError => e
44
- @logger.warn("Error while requiring file. No subscribers will run for this process: require = `#{@configuration.require}`, error_class = `#{e.class}`, error_message = `#{e.message}`, error_backtrace = `#{e.backtrace&.first(5)}`")
45
12
  end
46
13
  end
47
14
  end
@@ -1,9 +1,13 @@
1
1
  module Harmoniser
2
- Options = Data.define(:environment, :require, :verbose) do
2
+ Options = Data.define(:concurrency, :environment, :require, :verbose, :timeout) do
3
3
  def production?
4
4
  environment == "production"
5
5
  end
6
6
 
7
+ def unbounded_concurrency?
8
+ concurrency == Float::INFINITY
9
+ end
10
+
7
11
  def verbose?
8
12
  !!verbose
9
13
  end
@@ -8,6 +8,9 @@ module Harmoniser
8
8
  @options = {}
9
9
  @option_parser = OptionParser.new do |opts|
10
10
  opts.banner = "harmoniser [options]"
11
+ opts.on "-c", "--concurrency INT", "Set the number of threads to use" do |arg|
12
+ @options[:concurrency] = Integer(arg)
13
+ end
11
14
  opts.on "-e", "--environment ENV", "Set the application environment (defaults to inferred environment or 'production')" do |arg|
12
15
  @options[:environment] = arg
13
16
  end
@@ -1,10 +1,10 @@
1
- require "harmoniser/channelable"
1
+ require "harmoniser/connectable"
2
2
  require "harmoniser/definition"
3
3
 
4
4
  module Harmoniser
5
5
  module Publisher
6
6
  class MissingExchangeDefinition < StandardError; end
7
- include Channelable
7
+ include Connectable
8
8
 
9
9
  module ClassMethods
10
10
  def harmoniser_publisher(exchange_name:)
@@ -44,7 +44,7 @@ module Harmoniser
44
44
  end
45
45
 
46
46
  def raise_missing_exchange_definition
47
- raise MissingExchangeDefinition, "Please call the harmoniser_publisher class method at `#{const_get(:HARMONISER_PUBLISHER_CLASS)}` with the exchange_name that will be used for publishing"
47
+ raise MissingExchangeDefinition, "Please call the harmoniser_publisher class method at `#{name}` with the exchange_name that will be used for publishing"
48
48
  end
49
49
 
50
50
  def handle_return(exchange)
@@ -57,9 +57,15 @@ module Harmoniser
57
57
  class << self
58
58
  def included(base)
59
59
  base.const_set(:HARMONISER_PUBLISHER_MUTEX, Mutex.new)
60
- base.const_set(:HARMONISER_PUBLISHER_CLASS, base)
60
+ base.private_constant(:HARMONISER_PUBLISHER_MUTEX)
61
61
  base.extend(ClassMethods)
62
62
  end
63
63
  end
64
+
65
+ at_exit do
66
+ next if Harmoniser.server?
67
+ next unless Publisher.connection?
68
+ Publisher.connection.close
69
+ end
64
70
  end
65
71
  end
@@ -0,0 +1,19 @@
1
+ require "forwardable"
2
+
3
+ module Harmoniser
4
+ module Subscriber
5
+ class Registry
6
+ extend Forwardable
7
+
8
+ def_delegators :@klasses, :<<, :to_a, :each, :map
9
+
10
+ def initialize
11
+ @klasses = Set.new
12
+ end
13
+
14
+ def to_s
15
+ to_a.map(&:harmoniser_subscriber_to_s).to_s
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,12 +1,11 @@
1
- require "harmoniser/channelable"
1
+ require "harmoniser/connectable"
2
2
  require "harmoniser/definition"
3
- require "harmoniser/includable"
3
+ require "harmoniser/subscriber/registry"
4
4
 
5
5
  module Harmoniser
6
6
  module Subscriber
7
7
  class MissingConsumerDefinition < StandardError; end
8
- include Channelable
9
- include Includable
8
+ include Connectable
10
9
 
11
10
  module ClassMethods
12
11
  def harmoniser_subscriber(queue_name:, consumer_tag: nil, no_ack: true, exclusive: false, arguments: {})
@@ -19,22 +18,34 @@ module Harmoniser
19
18
  )
20
19
  end
21
20
 
22
- def harmoniser_subscriber_start
21
+ def harmoniser_subscriber_start(channel: nil)
23
22
  const_get(:HARMONISER_SUBSCRIBER_MUTEX).synchronize do
24
- @harmoniser_consumer ||= create_consumer
23
+ @harmoniser_consumer ||= create_consumer(channel)
25
24
  end
26
25
  end
27
26
 
27
+ def harmoniser_subscriber_stop
28
+ return unless @harmoniser_consumer
29
+ return unless @harmoniser_consumer.channel.open?
30
+
31
+ @harmoniser_consumer.cancel
32
+ end
33
+
34
+ def harmoniser_subscriber_to_s
35
+ definition = @harmoniser_consumer_definition
36
+ "<#{name}>: queue_name = `#{definition.queue_name}`, no_ack = `#{definition.no_ack}`"
37
+ end
38
+
28
39
  private
29
40
 
30
- def create_consumer
41
+ def create_consumer(channel)
31
42
  raise_missing_consumer_definition unless @harmoniser_consumer_definition
32
43
 
33
- channel = Subscriber.create_channel
44
+ ch = channel || Subscriber.create_channel
34
45
  consumer = Bunny::Consumer.new(
35
- channel,
46
+ ch,
36
47
  @harmoniser_consumer_definition.queue_name,
37
- @harmoniser_consumer_definition.consumer_tag || channel.generate_consumer_tag,
48
+ @harmoniser_consumer_definition.consumer_tag || ch.generate_consumer_tag,
38
49
  @harmoniser_consumer_definition.no_ack,
39
50
  @harmoniser_consumer_definition.exclusive,
40
51
  @harmoniser_consumer_definition.arguments
@@ -71,16 +82,20 @@ module Harmoniser
71
82
  end
72
83
 
73
84
  def raise_missing_consumer_definition
74
- raise MissingConsumerDefinition, "Please call the harmoniser_subscriber class method at `#{const_get(:HARMONISER_SUBSCRIBER_CLASS)}` with the queue_name that will be used for subscribing"
85
+ raise MissingConsumerDefinition, "Please call the harmoniser_subscriber class method at `#{name}` with the queue_name that will be used for subscribing"
75
86
  end
76
87
  end
77
88
 
78
89
  class << self
79
90
  def included(base)
80
91
  base.const_set(:HARMONISER_SUBSCRIBER_MUTEX, Mutex.new)
81
- base.const_set(:HARMONISER_SUBSCRIBER_CLASS, base)
92
+ base.private_constant(:HARMONISER_SUBSCRIBER_MUTEX)
93
+ registry << base
82
94
  base.extend(ClassMethods)
83
- harmoniser_register_included(base)
95
+ end
96
+
97
+ def registry
98
+ @registry ||= Registry.new
84
99
  end
85
100
  end
86
101
  end
@@ -1,9 +1,9 @@
1
- require "harmoniser/channelable"
1
+ require "harmoniser/connectable"
2
2
  require "harmoniser/definition"
3
3
 
4
4
  module Harmoniser
5
5
  class Topology
6
- include Channelable
6
+ include Connectable
7
7
 
8
8
  attr_reader :bindings, :exchanges, :queues
9
9
 
@@ -41,11 +41,12 @@ module Harmoniser
41
41
  end
42
42
 
43
43
  def declare
44
- channel = self.class.create_channel
45
- declare_exchanges(channel)
46
- declare_queues(channel)
47
- declare_bindings(channel)
48
- Harmoniser.connection.close
44
+ self.class.create_channel.tap do |ch|
45
+ declare_exchanges(ch)
46
+ declare_queues(ch)
47
+ declare_bindings(ch)
48
+ ch.connection.close
49
+ end
49
50
  end
50
51
 
51
52
  private
@@ -1,3 +1,3 @@
1
1
  module Harmoniser
2
- VERSION = "0.9.0"
2
+ VERSION = "0.11.0"
3
3
  end
@@ -0,0 +1,48 @@
1
+ require "delegate"
2
+
3
+ module Harmoniser
4
+ class WorkPoolReporter
5
+ def initialize(consumers:, logger: Harmoniser.logger)
6
+ @channels = build_channels(consumers)
7
+ @logger = logger
8
+ end
9
+
10
+ def to_s
11
+ "<#{self.class.name}>: #{channels_to_s}"
12
+ end
13
+
14
+ private
15
+
16
+ def channels_to_s
17
+ @channels.map do |(channel, queues)|
18
+ channel_info(channel, queues.to_a)
19
+ end
20
+ end
21
+
22
+ def channel_info(channel, queues)
23
+ work_pool = channel.work_pool
24
+ "<#{channel.id}>: backlog = `#{work_pool.backlog}`, running? = `#{work_pool.running?}`, queues = `#{queues}`"
25
+ end
26
+
27
+ def build_channels(consumers)
28
+ initial = Hash.new { |hash, key| hash[key] = Set.new }
29
+ consumers.each_with_object(initial) do |consumer, acc|
30
+ acc[DecoratedChannel.new(consumer.channel)] << consumer.queue
31
+ end
32
+ end
33
+
34
+ class DecoratedChannel < SimpleDelegator
35
+ def id
36
+ __getobj__.id
37
+ end
38
+
39
+ def hash
40
+ id.hash
41
+ end
42
+
43
+ def eql?(other)
44
+ other.is_a?(DecoratedChannel) && id == other.id
45
+ end
46
+ end
47
+ end
48
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harmoniser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Lloret
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-09 00:00:00.000000000 Z
11
+ date: 2024-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.22'
19
+ version: '2.23'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.22'
26
+ version: '2.23'
27
27
  description: A declarative approach to communication with RabbitMQ that simplifies
28
28
  the integration of publishing and consuming messages.
29
29
  email:
@@ -42,22 +42,25 @@ files:
42
42
  - bin/setup
43
43
  - harmoniser.gemspec
44
44
  - lib/harmoniser.rb
45
- - lib/harmoniser/channelable.rb
46
45
  - lib/harmoniser/cli.rb
47
46
  - lib/harmoniser/configurable.rb
48
47
  - lib/harmoniser/configuration.rb
49
48
  - lib/harmoniser/connectable.rb
50
49
  - lib/harmoniser/connection.rb
51
50
  - lib/harmoniser/definition.rb
52
- - lib/harmoniser/includable.rb
53
51
  - lib/harmoniser/launcher.rb
52
+ - lib/harmoniser/launcher/base.rb
53
+ - lib/harmoniser/launcher/bounded.rb
54
+ - lib/harmoniser/launcher/unbounded.rb
54
55
  - lib/harmoniser/loggable.rb
55
56
  - lib/harmoniser/options.rb
56
57
  - lib/harmoniser/parser.rb
57
58
  - lib/harmoniser/publisher.rb
58
59
  - lib/harmoniser/subscriber.rb
60
+ - lib/harmoniser/subscriber/registry.rb
59
61
  - lib/harmoniser/topology.rb
60
62
  - lib/harmoniser/version.rb
63
+ - lib/harmoniser/work_pool_reporter.rb
61
64
  homepage: https://github.com/jollopre/harmoniser
62
65
  licenses:
63
66
  - MIT
@@ -1,37 +0,0 @@
1
- module Harmoniser
2
- module Channelable
3
- MUTEX = Mutex.new
4
- private_constant :MUTEX
5
-
6
- module ClassMethods
7
- def harmoniser_channel
8
- MUTEX.synchronize do
9
- @harmoniser_channel ||= create_channel
10
- end
11
- end
12
-
13
- def create_channel
14
- channel = Harmoniser.connection.create_channel
15
- channel.on_error(&method(:on_error).to_proc)
16
- channel.on_uncaught_exception(&method(:on_uncaught_exception).to_proc)
17
- channel
18
- end
19
-
20
- private
21
-
22
- def on_error(channel, amq_method)
23
- Harmoniser.logger.error("Default on_error handler executed for channel: method = `#{amq_method}`, exchanges = `#{channel.exchanges.keys}`, queues = `#{channel.queues.keys}`")
24
- end
25
-
26
- def on_uncaught_exception(error, consumer)
27
- Harmoniser.logger.error("Default on_uncaught_exception handler executed for channel: error_class = `#{error.class}`, error_message = `#{error.message}`, error_backtrace = `#{error.backtrace&.first(5)}, queue = `#{consumer.queue}`")
28
- end
29
- end
30
-
31
- class << self
32
- def included(base)
33
- base.extend(ClassMethods)
34
- end
35
- end
36
- end
37
- end
@@ -1,25 +0,0 @@
1
- module Harmoniser
2
- module Includable
3
- MUTEX = Mutex.new
4
- private_constant :MUTEX
5
-
6
- module ClassMethods
7
- def harmoniser_register_included(klass)
8
- MUTEX.synchronize do
9
- @harmoniser_included ||= Set.new
10
- @harmoniser_included << klass
11
- end
12
- end
13
-
14
- def harmoniser_included
15
- @harmoniser_included.to_a
16
- end
17
- end
18
-
19
- class << self
20
- def included(base)
21
- base.extend(ClassMethods)
22
- end
23
- end
24
- end
25
- end