diff --git a/CHANGELOG.md b/CHANGELOG.md index e34eadd..1dedfc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.8.0] - 2024-03-31 + +### Added +- Implement retry mechanism to establish connection to RabbitMQ. More details at [issue](https://github.com/jollopre/harmoniser/issues/39). +- Strengthen at_exit hook to not break when connection cannot be closed. + ## [0.7.0] - 2024-01-03 ### Added diff --git a/Gemfile.lock b/Gemfile.lock index e8cdadf..b9c97cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - harmoniser (0.7.0) + harmoniser (0.8.0) bunny (~> 2.22) GEM diff --git a/lib/harmoniser/cli.rb b/lib/harmoniser/cli.rb index cd58495..f6bd840 100644 --- a/lib/harmoniser/cli.rb +++ b/lib/harmoniser/cli.rb @@ -22,7 +22,6 @@ def initialize def call parse_options - define_signals run end @@ -50,6 +49,8 @@ def run .new(configuration: configuration, logger: logger) .start + define_signals + while @read_io.wait_readable signal = @read_io.gets.strip handle_signal(signal) diff --git a/lib/harmoniser/connectable.rb b/lib/harmoniser/connectable.rb index f0d248d..d61dd3b 100644 --- a/lib/harmoniser/connectable.rb +++ b/lib/harmoniser/connectable.rb @@ -37,11 +37,10 @@ def at_exit_handler logger = Harmoniser.logger logger.info("Shutting down!") - if connection? && connection.open? - stringified_connection = connection.to_s - logger.info("Connection will be closed: connection = `#{stringified_connection}`") - connection.close - logger.info("Connection closed: connection = `#{stringified_connection}`") + if connection? && @connection.open? + logger.info("Connection will be closed: connection = `#{@connection}`") + @connection.close + logger.info("Connection closed: connection = `#{@connection}`") end logger.info("Bye!") end diff --git a/lib/harmoniser/connection.rb b/lib/harmoniser/connection.rb index 44cde14..cfe4a58 100644 --- a/lib/harmoniser/connection.rb +++ b/lib/harmoniser/connection.rb @@ -27,7 +27,7 @@ class Connection write_timeout: 5 } - def_delegators :@bunny, :close, :create_channel, :open?, :recovering_from_network_failure?, :start + def_delegators :@bunny, :create_channel, :open?, :recovering_from_network_failure? def initialize(opts) @bunny = Bunny.new(opts) @@ -37,6 +37,26 @@ def to_s "<#{self.class.name}>: #{user}@#{host}:#{port}, connection_name = `#{connection_name}`, vhost = `#{vhost}`" end + def start + retries = 0 + begin + with_signal_handler { @bunny.start } + rescue => e + Harmoniser.logger.error("Connection attempt failed: retries = `#{retries}`, error_class = `#{e.class}`, error_message = `#{e.message}`") + with_signal_handler { sleep(1) } + retries += 1 + retry + end + end + + def close + @bunny.close + true + rescue => e + Harmoniser.logger.error("Connection#close failed: error_class = `#{e.class}`, error_message = `#{e.message}`") + false + end + private def connection_name @@ -58,5 +78,12 @@ def user def vhost @bunny.vhost end + + def with_signal_handler + yield if block_given? + rescue SignalException => e + Harmoniser.logger.info("Signal received: signal = `#{Signal.signame(e.signo)}`") + exit(0) + end end end diff --git a/lib/harmoniser/version.rb b/lib/harmoniser/version.rb index 66f2d2a..093d89c 100644 --- a/lib/harmoniser/version.rb +++ b/lib/harmoniser/version.rb @@ -1,3 +1,3 @@ module Harmoniser - VERSION = "0.7.0" + VERSION = "0.8.0" end diff --git a/spec/harmoniser/connection_spec.rb b/spec/harmoniser/connection_spec.rb index bbb5558..6c2c61b 100644 --- a/spec/harmoniser/connection_spec.rb +++ b/spec/harmoniser/connection_spec.rb @@ -20,13 +20,60 @@ expect(subject).to respond_to(:recovering_from_network_failure?) end - it "responds to start" do - expect(subject).to respond_to(:start) - end - describe "#to_s" do it "returns a string representation of the connection" do expect(subject.to_s).to eq(": guest@127.0.0.1:5672, connection_name = `wadus`, vhost = `/`") end end + + describe "#start" do + let(:bunny) { subject.instance_variable_get(:@bunny) } + + it "retries establishing connection until succeeding" do + allow(Harmoniser.logger).to receive(:error) + allow(subject).to receive(:sleep) + allow(bunny).to receive(:start) do + @retries ||= 0 + if @retries < 2 + @retries += 1 + raise "Error" + end + end + + subject.start + + expect(Harmoniser.logger).to have_received(:error).with(/Connection attempt failed: retries = `.*`, error_class = `RuntimeError`, error_message = `Error`/).twice + end + end + + describe "#close" do + let(:bunny) { subject.instance_variable_get(:@bunny) } + + it "returns true" do + allow(bunny).to receive(:close) + + result = subject.close + + expect(result).to eq(true) + end + + context "when closing connection fails" do + it "returns false" do + allow(bunny).to receive(:close).and_raise("Error") + + result = subject.close + + expect(result).to eq(false) + end + + it "log with error severity is output" do + allow(bunny).to receive(:close).and_raise("Error") + allow(Harmoniser.logger).to receive(:error) + + subject.close + + expect(Harmoniser.logger).to have_received(:error).with(/Connection#close failed: error_class = `RuntimeError`, error_message = `Error`/) + end + end + end end diff --git a/spec/shared_context/configurable.rb b/spec/shared_context/configurable.rb index 70ea61a..e935290 100644 --- a/spec/shared_context/configurable.rb +++ b/spec/shared_context/configurable.rb @@ -1,6 +1,5 @@ RSpec.shared_context "configurable" do let(:host) { ENV.fetch("RABBITMQ_HOST") } - let(:bunny) { Bunny.new(host: host, logger: Logger.new(IO::NULL)).start } before do Harmoniser.configure do |config| @@ -18,4 +17,17 @@ def declare_queue(name, exchange_name) channel = bunny.create_channel Bunny::Queue.new(channel, name, {auto_delete: true}).bind(exchange_name) end + + def bunny + @bunny ||= Bunny.new(host: host, logger: Logger.new(IO::NULL)) + return @bunny if @bunny.open? + + begin + @bunny.start + rescue => e + puts "start connection attempt failed: error_class = `#{e.class}`, error_message = `#{e.message}`" + sleep(1) + retry + end + end end