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 +4 -4
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +0 -6
- data/lib/harmoniser/configurable.rb +0 -5
- data/lib/harmoniser/configuration.rb +11 -2
- data/lib/harmoniser/connectable.rb +58 -25
- data/lib/harmoniser/connection.rb +34 -8
- data/lib/harmoniser/launcher/base.rb +12 -7
- data/lib/harmoniser/launcher/bounded.rb +1 -4
- data/lib/harmoniser/publisher.rb +8 -2
- data/lib/harmoniser/subscriber.rb +2 -2
- data/lib/harmoniser/topology.rb +3 -3
- data/lib/harmoniser/version.rb +1 -1
- metadata +2 -3
- data/lib/harmoniser/channelable.rb +0 -70
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1a7c351ba8ed80091b6e32dbd3cb0486f6b4123b30de12f1406862f3bdedcbb
|
4
|
+
data.tar.gz: f3d68be4e8cc7cb83e5df89bbc20ce4bc905b44711d59a23de50e7ac761bc5ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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/
|
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
|
-
|
8
|
-
|
9
|
-
.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
23
|
-
|
16
|
+
def connection?
|
17
|
+
!!defined?(@connection)
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
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
|
-
@
|
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}
|
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
|
-
|
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
|
-
@
|
47
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
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 ||=
|
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
|
data/lib/harmoniser/publisher.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
require "harmoniser/
|
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
|
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/
|
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
|
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: {})
|
data/lib/harmoniser/topology.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
require "harmoniser/
|
1
|
+
require "harmoniser/connectable"
|
2
2
|
require "harmoniser/definition"
|
3
3
|
|
4
4
|
module Harmoniser
|
5
5
|
class Topology
|
6
|
-
include
|
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
|
|
data/lib/harmoniser/version.rb
CHANGED
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.
|
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
|
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
|