Skip to content

Commit

Permalink
feat: [#39] Introduce retry pattern for establishing connection to (#40)
Browse files Browse the repository at this point in the history
RabbitMQ

* Implement retry for Harmoniser::Connection#start
* Prevent Harmoniser::Connection#close to raise exception. Prefer log to
  error the exception and terminate gracefully
  • Loading branch information
jollopre authored Mar 31, 2024
1 parent f783ab8 commit e0aeac1
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 14 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
harmoniser (0.7.0)
harmoniser (0.8.0)
bunny (~> 2.22)

GEM
Expand Down
3 changes: 2 additions & 1 deletion lib/harmoniser/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ def initialize

def call
parse_options
define_signals
run
end

Expand Down Expand Up @@ -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)
Expand Down
9 changes: 4 additions & 5 deletions lib/harmoniser/connectable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion lib/harmoniser/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/harmoniser/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Harmoniser
VERSION = "0.7.0"
VERSION = "0.8.0"
end
55 changes: 51 additions & 4 deletions spec/harmoniser/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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("<Harmoniser::Connection>: [email protected]: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
14 changes: 13 additions & 1 deletion spec/shared_context/configurable.rb
Original file line number Diff line number Diff line change
@@ -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|
Expand All @@ -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

0 comments on commit e0aeac1

Please sign in to comment.