harmoniser 0.9.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 +29 -0
- data/LICENSE.txt +1 -1
- data/README.md +0 -6
- data/harmoniser.gemspec +1 -1
- data/lib/harmoniser/cli.rb +12 -5
- data/lib/harmoniser/configurable.rb +0 -5
- data/lib/harmoniser/configuration.rb +14 -3
- data/lib/harmoniser/connectable.rb +54 -29
- data/lib/harmoniser/connection.rb +37 -17
- data/lib/harmoniser/launcher/base.rb +80 -0
- data/lib/harmoniser/launcher/bounded.rb +19 -0
- data/lib/harmoniser/launcher/unbounded.rb +11 -0
- data/lib/harmoniser/launcher.rb +8 -41
- data/lib/harmoniser/options.rb +5 -1
- data/lib/harmoniser/parser.rb +3 -0
- data/lib/harmoniser/publisher.rb +10 -4
- data/lib/harmoniser/subscriber/registry.rb +19 -0
- data/lib/harmoniser/subscriber.rb +28 -13
- data/lib/harmoniser/topology.rb +8 -7
- data/lib/harmoniser/version.rb +1 -1
- data/lib/harmoniser/work_pool_reporter.rb +48 -0
- metadata +9 -6
- data/lib/harmoniser/channelable.rb +0 -37
- data/lib/harmoniser/includable.rb +0 -25
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,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
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
data/lib/harmoniser/cli.rb
CHANGED
@@ -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
|
-
|
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/
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
12
|
-
|
16
|
+
def connection?
|
17
|
+
!!defined?(@connection)
|
18
|
+
end
|
13
19
|
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
63
|
+
amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
|
64
|
+
end
|
65
|
+
end
|
38
66
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
-
@
|
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}
|
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
|
-
|
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
|
-
@
|
54
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/harmoniser/launcher.rb
CHANGED
@@ -1,47 +1,14 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
data/lib/harmoniser/options.rb
CHANGED
@@ -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
|
data/lib/harmoniser/parser.rb
CHANGED
@@ -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
|
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:)
|
@@ -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 `#{
|
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.
|
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/
|
1
|
+
require "harmoniser/connectable"
|
2
2
|
require "harmoniser/definition"
|
3
|
-
require "harmoniser/
|
3
|
+
require "harmoniser/subscriber/registry"
|
4
4
|
|
5
5
|
module Harmoniser
|
6
6
|
module Subscriber
|
7
7
|
class MissingConsumerDefinition < StandardError; end
|
8
|
-
include
|
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
|
-
|
44
|
+
ch = channel || Subscriber.create_channel
|
34
45
|
consumer = Bunny::Consumer.new(
|
35
|
-
|
46
|
+
ch,
|
36
47
|
@harmoniser_consumer_definition.queue_name,
|
37
|
-
@harmoniser_consumer_definition.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 `#{
|
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.
|
92
|
+
base.private_constant(:HARMONISER_SUBSCRIBER_MUTEX)
|
93
|
+
registry << base
|
82
94
|
base.extend(ClassMethods)
|
83
|
-
|
95
|
+
end
|
96
|
+
|
97
|
+
def registry
|
98
|
+
@registry ||= Registry.new
|
84
99
|
end
|
85
100
|
end
|
86
101
|
end
|
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
|
|
@@ -41,11 +41,12 @@ module Harmoniser
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def declare
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
data/lib/harmoniser/version.rb
CHANGED
@@ -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.
|
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-
|
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.
|
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.
|
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
|