harmoniser 0.10.0 → 0.11.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: f94dc99fc3550a6ad359ffed69a06652849e74317ef607bedbaadad9c09f44fa
4
- data.tar.gz: 489bea79098e457c8391f4fe1949b00e1618818b67fbff1fceb40da89ee48da8
3
+ metadata.gz: a1a7c351ba8ed80091b6e32dbd3cb0486f6b4123b30de12f1406862f3bdedcbb
4
+ data.tar.gz: f3d68be4e8cc7cb83e5df89bbc20ce4bc905b44711d59a23de50e7ac761bc5ae
5
5
  SHA512:
6
- metadata.gz: 7b78d93705c81e6dc30ddba7dc02733476a40998568299df5b2742ef80c47f2fba6bfb03e1d272f69472030ab19b40276c23beba88505d40fe86c65685855efd
7
- data.tar.gz: d0ac0e8502cc760826e22d446ae914eacdca0f8b01faec19dc52350f994cd5c1fb3d663e4d2465bec6c26c5bdbb22e9f9278d8eff60521c08147f8c6cb743425
6
+ metadata.gz: 5c6da5317a0ea480766ce716b7710598d6a1720afd8bb24cf82f2aecd3b96e9d41b31b3246d306c525e5ddbb5532f8adb87c7b3188a402ecada8c87a192176fb
7
+ data.tar.gz: 9a31ed8616f30072245dbc783692cadf871c46f2082f627420fd71a38f6e29b3d9162b6152fdf741173a67e4a16fb5d1c8fbe812eaffb081356f4b83796ad510
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## [0.10.0] - 2024-09-16
4
14
 
5
15
  ### 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).
@@ -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,12 +1,11 @@
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
11
  def_delegators :options, :concurrency, :environment, :require, :verbose, :timeout
@@ -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
 
@@ -4,37 +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
9
- .merge({
10
- logger: Harmoniser.logger,
11
- recovery_attempt_started: proc {
12
- stringified_connection = connection.to_s
13
- Harmoniser.logger.info("Recovery attempt started: connection = `#{stringified_connection}`")
14
- },
15
- recovery_completed: proc {
16
- stringified_connection = connection.to_s
17
- Harmoniser.logger.info("Recovery completed: connection = `#{stringified_connection}`")
18
- }
19
- })
20
- 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
21
15
 
22
- def connection_opts=(opts)
23
- raise TypeError, "opts must be a Hash object" unless opts.is_a?(Hash)
16
+ def connection?
17
+ !!defined?(@connection)
18
+ end
24
19
 
25
- @connection_opts = connection_opts.merge(opts)
26
- 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
27
+
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)
33
+ end
34
+
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
+ }
27
41
 
28
- def connection
29
- MUTEX.synchronize do
30
- @connection ||= Connection.new(connection_opts)
31
- @connection.start unless @connection.open? || @connection.recovering_from_network_failure?
32
- @connection
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
46
+
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)
62
+
63
+ amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
33
64
  end
34
65
  end
35
66
 
36
- def connection?
37
- !!defined?(@connection)
67
+ class << self
68
+ def included(base)
69
+ base.extend(ClassMethods)
70
+ end
38
71
  end
39
72
  end
40
73
  end
@@ -21,12 +21,15 @@ module Harmoniser
21
21
 
22
22
  def_delegators :@bunny, :create_channel, :open?, :recovering_from_network_failure?
23
23
 
24
- def initialize(opts)
25
- @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
26
29
  end
27
30
 
28
31
  def to_s
29
- "<#{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}`"
30
33
  end
31
34
 
32
35
  # TODO Only perform retries when Harmoniser.server?
@@ -35,7 +38,7 @@ module Harmoniser
35
38
  begin
36
39
  with_signal_handler { @bunny.start }
37
40
  rescue => e
38
- 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}`")
39
42
  with_signal_handler { sleep(1) }
40
43
  retries += 1
41
44
  retry
@@ -43,15 +46,26 @@ module Harmoniser
43
46
  end
44
47
 
45
48
  def close
46
- @bunny.close
47
- true
49
+ @logger.info("Connection will be closed: connection = `#{self}`")
50
+ @bunny.close.tap do
51
+ @logger.info("Connection closed: connection = `#{self}`")
52
+ end
48
53
  rescue => e
49
- 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}`")
50
55
  false
51
56
  end
52
57
 
53
58
  private
54
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
+
55
69
  def connection_name
56
70
  @bunny.connection_name
57
71
  end
@@ -60,6 +74,18 @@ module Harmoniser
60
74
  @bunny.transport.host
61
75
  end
62
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
+
63
89
  def port
64
90
  @bunny.transport.port
65
91
  end
@@ -76,7 +102,7 @@ module Harmoniser
76
102
  def with_signal_handler
77
103
  yield if block_given?
78
104
  rescue SignalException => e
79
- Harmoniser.logger.info("Signal received: signal = `#{Signal.signame(e.signo)}`")
105
+ @logger.info("Signal received: signal = `#{Signal.signame(e.signo)}`")
80
106
  Harmoniser.server? ? exit(0) : raise
81
107
  end
82
108
  end
@@ -49,16 +49,21 @@ module Harmoniser
49
49
  end
50
50
 
51
51
  def maybe_close
52
- return unless @configuration.connection?
53
- return unless @configuration.connection.open?
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?
54
63
 
55
64
  maybe_cancel_subscribers
56
65
  report_work_pool
57
-
58
- connection = @configuration.connection
59
- @logger.info("Connection will be closed: connection = `#{connection}`")
60
- connection.close
61
- @logger.info("Connection closed: connection = `#{connection}`")
66
+ Subscriber.connection.close
62
67
  end
63
68
 
64
69
  def maybe_cancel_subscribers
@@ -1,11 +1,8 @@
1
- require "harmoniser/channelable"
2
1
  require_relative "base"
3
2
 
4
3
  module Harmoniser
5
4
  module Launcher
6
5
  class Bounded < Base
7
- include Channelable
8
-
9
6
  private
10
7
 
11
8
  def start_subscribers
@@ -15,7 +12,7 @@ module Harmoniser
15
12
  end
16
13
 
17
14
  def channel
18
- @channel ||= self.class.create_channel(consumer_pool_size: @configuration.concurrency, consumer_pool_shutdown_timeout: @configuration.timeout)
15
+ @channel ||= Subscriber.create_channel(consumer_pool_size: @configuration.concurrency, consumer_pool_shutdown_timeout: @configuration.timeout)
19
16
  end
20
17
  end
21
18
  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:)
@@ -61,5 +61,11 @@ module Harmoniser
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
@@ -1,11 +1,11 @@
1
- require "harmoniser/channelable"
1
+ require "harmoniser/connectable"
2
2
  require "harmoniser/definition"
3
3
  require "harmoniser/subscriber/registry"
4
4
 
5
5
  module Harmoniser
6
6
  module Subscriber
7
7
  class MissingConsumerDefinition < StandardError; end
8
- include Channelable
8
+ include Connectable
9
9
 
10
10
  module ClassMethods
11
11
  def harmoniser_subscriber(queue_name:, consumer_tag: nil, no_ack: true, exclusive: false, arguments: {})
@@ -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
 
@@ -45,7 +45,7 @@ module Harmoniser
45
45
  declare_exchanges(ch)
46
46
  declare_queues(ch)
47
47
  declare_bindings(ch)
48
- ch.close
48
+ ch.connection.close
49
49
  end
50
50
  end
51
51
 
@@ -1,3 +1,3 @@
1
1
  module Harmoniser
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.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.10.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-09-16 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
@@ -42,7 +42,6 @@ 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
@@ -1,70 +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(consumer_pool_size: 1, consumer_pool_shutdown_timeout: 60)
14
- connection
15
- .create_channel(nil, consumer_pool_size, false, consumer_pool_shutdown_timeout)
16
- .tap do |channel|
17
- attach_callbacks(channel)
18
- end
19
- end
20
-
21
- private
22
-
23
- def connection
24
- Harmoniser.connection
25
- end
26
-
27
- def attach_callbacks(channel)
28
- channel.on_error(&method(:on_error).to_proc)
29
- channel.on_uncaught_exception(&method(:on_uncaught_exception).to_proc)
30
- end
31
-
32
- def on_error(channel, amq_method)
33
- attributes = {
34
- amq_method: amq_method,
35
- exchanges: channel.exchanges.keys,
36
- queues: channel.consumers.values.map(&:queue)
37
- }
38
-
39
- if amq_method.is_a?(AMQ::Protocol::Channel::Close)
40
- attributes[:reply_code] = amq_method.reply_code
41
- attributes[:reply_text] = amq_method.reply_text
42
- end
43
-
44
- stringified_attributes = attributes.map { |k, v| "#{k} = `#{v}`" }.join(", ")
45
- Harmoniser.logger.error("Default on_error handler executed for channel: #{stringified_attributes}")
46
- maybe_kill_process(amq_method)
47
- end
48
-
49
- def on_uncaught_exception(error, consumer)
50
- 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}`")
51
- end
52
-
53
- def maybe_kill_process(amq_method)
54
- Process.kill("USR1", Process.pid) if ack_timed_out?(amq_method) && Harmoniser.server?
55
- end
56
-
57
- def ack_timed_out?(amq_method)
58
- return false unless amq_method.is_a?(AMQ::Protocol::Channel::Close)
59
-
60
- amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
61
- end
62
- end
63
-
64
- class << self
65
- def included(base)
66
- base.extend(ClassMethods)
67
- end
68
- end
69
- end
70
- end