harmoniser 0.12.0 → 0.13.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: f9feb1146437db2d23bb56ef1a72e47ef0912dbbc02b776c2ef026db44dc687e
4
- data.tar.gz: 79dfe110ea228ba5ae58062c575768c8cc5747228a7a4a5432c36a686da7113d
3
+ metadata.gz: 71c030f6ce0d93a7e9bb55a88c3d3cad300f4ab05809944e08442d2bca43cafb
4
+ data.tar.gz: da8f3f99f2a9846b35b10a8e986f28e53087a81d2a1a6fd149778e3d82222de1
5
5
  SHA512:
6
- metadata.gz: 47e94a951a8200b1e2ce2287f4fad5d4dda4ac6d4bf4d4e21a7f0bb03a51c8d95a949f0ef8dccc1f873558741ece21a0de89198d0d4325228869b0ad985d098a
7
- data.tar.gz: d7d11b42621ce764702a408a3c1f789326cef84dfa15ad24d6f8c84ba5b2cd28ce4f4ca1496fd38bfc1aedb1a0d89381adf19825c76ef6d310389173782a03bd
6
+ metadata.gz: 0774eab4d7682a72e880e764acdb55e05e6e9d10048bbdb3e74ed010293d6dc8c416d9de9d73c3a6752e3c9d1ace45d70dc212a3cc01fb2a33b77d4a494d56d2
7
+ data.tar.gz: 2eebe48b9779d08b0e516d13daa9f94b358f851de3999d4ca0ee600cab89ae48396bdd351ff583bc9be566e44f113f2fe14c0ef59ce3378d5fe1f35d88802c83
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0] - 2025-05-19
4
+
5
+ ### Added
6
+ - Introduce Harmoniser.configuration#on_error callback to handle errors that occur while running the application, (e.g. when a subscriber raises an error, or when a connection cannot be established to RabbitMQ). This method permits appending as many handlers as needed. Usage:
7
+
8
+ ```ruby
9
+ Harmoniser.configuration do |config|
10
+ config.on_error do |error, ctx|
11
+ # error is the exception, i.e. StandardError class raised by the application whereas ctx is the context of the error passed as Hash. The ctx hash contains at least the key `description` which is a string describing the error.
12
+
13
+ puts "An error occurred: exception = `#{error.detailed_message}`; description = `#{ctx[:description]}`"
14
+ end
15
+ end
16
+ ```
17
+
18
+ ### Changed
19
+ - Update Bunny to the latest version, i.e., 2.24
20
+ - Delegate into Bunny::Channel to cancel consumers and close channel through
21
+ Channel#cancel_consumers_before_closing!
22
+ - Change severity level from error to warning for errors occurring at channel level
23
+
24
+
3
25
  ## [0.12.0] - 2024-12-22
4
26
 
5
27
  ### Changed
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.23"
27
+ spec.add_runtime_dependency "bunny", "~> 2.24"
28
28
  end
@@ -1,5 +1,6 @@
1
1
  require "forwardable"
2
2
  require "harmoniser/connection"
3
+ require "harmoniser/error_handler"
3
4
  require "harmoniser/topology"
4
5
  require "harmoniser/options"
5
6
 
@@ -7,14 +8,16 @@ module Harmoniser
7
8
  class Configuration
8
9
  extend Forwardable
9
10
 
10
- attr_reader :logger, :options
11
+ attr_reader :error_handler, :logger, :options
11
12
  def_delegators :options, :concurrency, :environment, :require, :verbose, :timeout
13
+ def_delegators :error_handler, :handle_error, :on_error
12
14
 
13
15
  def initialize
14
16
  @logger = Harmoniser.logger
15
- @options = Options.new(**default_options)
17
+ @options = Options.new
16
18
  set_logger_severity
17
19
  @topology = Topology.new
20
+ @error_handler = ErrorHandler.default
18
21
  end
19
22
 
20
23
  def connection_opts
@@ -40,16 +43,6 @@ module Harmoniser
40
43
 
41
44
  private
42
45
 
43
- def default_options
44
- {
45
- concurrency: Float::INFINITY,
46
- environment: ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "production")),
47
- require: ".",
48
- timeout: 25,
49
- verbose: false
50
- }
51
- end
52
-
53
46
  def set_logger_severity
54
47
  @logger.level = @options.verbose? ? Logger::DEBUG : Logger::INFO
55
48
  end
@@ -7,7 +7,7 @@ module Harmoniser
7
7
  module ClassMethods
8
8
  def connection(configuration = Harmoniser.configuration)
9
9
  MUTEX.synchronize do
10
- @connection ||= Connection.new(configuration.connection_opts)
10
+ @connection ||= Connection.new(configuration.connection_opts, error_handler: configuration.error_handler)
11
11
  @connection.start unless @connection.open? || @connection.recovering_from_network_failure?
12
12
  @connection
13
13
  end
@@ -21,6 +21,7 @@ module Harmoniser
21
21
  connection
22
22
  .create_channel(nil, consumer_pool_size, false, consumer_pool_shutdown_timeout)
23
23
  .tap do |channel|
24
+ channel.cancel_consumers_before_closing!
24
25
  attach_callbacks(channel)
25
26
  end
26
27
  end
@@ -45,12 +46,12 @@ module Harmoniser
45
46
  end
46
47
 
47
48
  stringified_attributes = attributes.map { |k, v| "#{k} = `#{v}`" }.join(", ")
48
- Harmoniser.logger.error("Default on_error handler executed for channel: #{stringified_attributes}")
49
+ Harmoniser.logger.warn("Default on_error handler executed for channel: #{stringified_attributes}")
49
50
  maybe_kill_process(amq_method)
50
51
  end
51
52
 
52
53
  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
+ handle_error(error, {description: "Uncaught exception from consumer", arguments: consumer.arguments, channel_id: consumer.channel.id, consumer_tag: consumer.consumer_tag, exclusive: consumer.exclusive, no_ack: consumer.no_ack, queue: consumer.queue})
54
55
  end
55
56
 
56
57
  def maybe_kill_process(amq_method)
@@ -62,6 +63,10 @@ module Harmoniser
62
63
 
63
64
  amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
64
65
  end
66
+
67
+ def handle_error(exception, ctx)
68
+ Harmoniser.configuration.handle_error(exception, ctx)
69
+ end
65
70
  end
66
71
 
67
72
  class << self
@@ -1,5 +1,6 @@
1
1
  require "forwardable"
2
2
  require "bunny"
3
+ require "harmoniser/error_handler"
3
4
 
4
5
  module Harmoniser
5
6
  class Connection
@@ -21,7 +22,8 @@ module Harmoniser
21
22
 
22
23
  def_delegators :@bunny, :create_channel, :open?, :recovering_from_network_failure?
23
24
 
24
- def initialize(opts, logger: Harmoniser.logger)
25
+ def initialize(opts, error_handler: ErrorHandler.default, logger: Harmoniser.logger)
26
+ @error_handler = error_handler
25
27
  @logger = logger
26
28
  @bunny = Bunny.new(maybe_dynamic_opts(opts)).tap do |bunny|
27
29
  attach_callbacks(bunny)
@@ -43,7 +45,7 @@ module Harmoniser
43
45
  begin
44
46
  @bunny.start
45
47
  rescue => e
46
- @logger.error("Connection attempt failed: retries = `#{retries}`, error_class = `#{e.class}`, error_message = `#{e.message}`")
48
+ handle_error(e, {description: "Connection attempt failed", retries: retries})
47
49
  sleep(1)
48
50
  retries += 1
49
51
  retry
@@ -56,7 +58,7 @@ module Harmoniser
56
58
  @logger.info("Connection closed: connection = `#{self}`")
57
59
  end
58
60
  rescue => e
59
- @logger.error("Connection#close failed: error_class = `#{e.class}`, error_message = `#{e.message}`")
61
+ handle_error(e, {description: "Connection close failed"})
60
62
  false
61
63
  end
62
64
 
@@ -102,5 +104,9 @@ module Harmoniser
102
104
  def vhost
103
105
  @bunny.vhost
104
106
  end
107
+
108
+ def handle_error(exception, ctx)
109
+ @error_handler.handle_error(exception, ctx)
110
+ end
105
111
  end
106
112
  end
@@ -0,0 +1,51 @@
1
+ module Harmoniser
2
+ class ErrorHandler
3
+ TIMEOUT = 25
4
+ DEFAULT_ERROR_HANDLER = ->(exception, ctx) do
5
+ Harmoniser.logger.error("Error handler called: exception = `#{exception.detailed_message}`, context = `#{ctx}`")
6
+ end
7
+
8
+ class << self
9
+ def default
10
+ @default ||= new([DEFAULT_ERROR_HANDLER])
11
+ end
12
+ end
13
+
14
+ def initialize(handlers = [])
15
+ @handlers = handlers
16
+ end
17
+
18
+ def on_error(handler = nil, &block)
19
+ h = handler || block
20
+ raise ArgumentError, "Please, provide a handler or a block" if h.nil?
21
+ raise TypeError, "Handler must respond to call" unless h.respond_to?(:call)
22
+
23
+ @handlers << h
24
+ self
25
+ end
26
+
27
+ def handle_error(exception, ctx = {})
28
+ coordinator = Thread::Queue.new
29
+
30
+ Thread.new do
31
+ handlers
32
+ .each { |handler| trigger_handler(handler, exception, ctx) }
33
+ .tap { coordinator.push(true) }
34
+ end.tap do |thread|
35
+ thread.report_on_exception = true
36
+ end
37
+
38
+ !!coordinator.pop(timeout: TIMEOUT)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :handlers
44
+
45
+ def trigger_handler(handler, exception, ctx)
46
+ handler.call(exception, ctx)
47
+ rescue => e
48
+ warn "An error occurred while handling a previous error: #{e.detailed_message}"
49
+ end
50
+ end
51
+ end
@@ -61,15 +61,13 @@ module Harmoniser
61
61
  def maybe_close_subscriber
62
62
  return unless Subscriber.connection?
63
63
 
64
- maybe_cancel_subscribers
64
+ report_subscribers_to_cancel
65
65
  report_work_pool
66
66
  Subscriber.connection.close
67
67
  end
68
68
 
69
- def maybe_cancel_subscribers
69
+ def report_subscribers_to_cancel
70
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
71
  end
74
72
 
75
73
  def report_work_pool
@@ -1,5 +1,21 @@
1
1
  module Harmoniser
2
- Options = Data.define(:concurrency, :environment, :require, :verbose, :timeout) do
2
+ class Options < Data.define(:concurrency, :environment, :require, :verbose, :timeout)
3
+ DEFAULT = {
4
+ concurrency: -> { Float::INFINITY },
5
+ environment: -> { ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "production")) },
6
+ require: -> { "." },
7
+ timeout: -> { 25 },
8
+ verbose: -> { false }
9
+ }.freeze
10
+
11
+ def initialize(**kwargs)
12
+ options = DEFAULT
13
+ .map { |k, v| [k, v.call] }
14
+ .to_h
15
+ .merge(kwargs)
16
+ super(**options)
17
+ end
18
+
3
19
  def production?
4
20
  environment == "production"
5
21
  end
@@ -1,3 +1,3 @@
1
1
  module Harmoniser
2
- VERSION = "0.12.0"
2
+ VERSION = "0.13.0"
3
3
  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.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Lloret
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-22 00:00:00.000000000 Z
11
+ date: 2025-05-19 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.23'
19
+ version: '2.24'
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.23'
26
+ version: '2.24'
27
27
  description: A declarative approach to communication with RabbitMQ that simplifies
28
28
  the integration of publishing and consuming messages.
29
29
  email:
@@ -48,6 +48,7 @@ files:
48
48
  - lib/harmoniser/connectable.rb
49
49
  - lib/harmoniser/connection.rb
50
50
  - lib/harmoniser/definition.rb
51
+ - lib/harmoniser/error_handler.rb
51
52
  - lib/harmoniser/launcher.rb
52
53
  - lib/harmoniser/launcher/base.rb
53
54
  - lib/harmoniser/launcher/bounded.rb
@@ -68,7 +69,7 @@ metadata:
68
69
  homepage_uri: https://github.com/jollopre/harmoniser
69
70
  source_code_uri: https://github.com/jollopre/harmoniser
70
71
  changelog_uri: https://github.com/jollopre/harmoniser/blob/master/CHANGELOG.md
71
- post_install_message:
72
+ post_install_message:
72
73
  rdoc_options: []
73
74
  require_paths:
74
75
  - lib
@@ -84,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
85
  version: '0'
85
86
  requirements: []
86
87
  rubygems_version: 3.4.10
87
- signing_key:
88
+ signing_key:
88
89
  specification_version: 4
89
90
  summary: A minimalistic approach to interact with RabbitMQ
90
91
  test_files: []