diff --git a/.gitignore b/.gitignore index a86133e4..c6fe15e1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ Gemfile.lock .tags .tags_sorted_by_file .Apple* -/bin/* .bundle/* vendor/* playground/* diff --git a/README.md b/README.md index 0fd4265a..6c939b03 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Specific examples: Modern Bunny versions support - * CRuby 2.7 through 3.3 (inclusive) + * CRuby 3.2 through 3.4 (inclusive) * [TruffleRuby](https://www.graalvm.org/ruby/) For environments that use TLS, Bunny expects Ruby installations to use a recent enough OpenSSL version that @@ -55,16 +55,20 @@ For environments that use TLS, Bunny expects Ruby installations to use a recent ### JRuby -Bunny works sufficiently well on JRuby but there are known -JRuby bugs in versions prior to JRuby 9000 that cause high CPU burn. JRuby users should -use [March Hare](http://rubymarchhare.info). +Bunny no longer supports JRuby. -Bunny `1.7.x` was the last version to support CRuby 1.9.3 and 1.8.7 +JRuby users should use [March Hare](http://rubymarchhare.info), which has a similar API +and is built on top of the RabbitMQ Java client specifically for JRuby. ## Supported RabbitMQ Versions -Modern Bunny releases target [currently supported RabbitMQ release series](https://www.rabbitmq.com/versions.html). +Modern Bunny releases target [currently community supported RabbitMQ release series](https://www.rabbitmq.com/release-information). + +The protocol implemented by Bunny was first introduced in RabbitMQ 2.0 and has evolved +via extensions and with next to no breaking changes, so all key Bunny operations can be used with a wide range +of RabbitMQ versions, accounting for the few potentially breaking changes they +may introduce, e.g. the idempotency of `queue.delete` operations. ## Change Log @@ -143,7 +147,7 @@ conn.close ### Getting Started -For a 15 minute tutorial using more practical examples, see [Getting Started with RabbitMQ and Ruby using Bunny](http://rubybunny.info/articles/getting_started.html). +For a 15 minute tutorial using more practical examples, see [Getting Started with RabbitMQ and Ruby using Bunny](https://github.com/ruby-amqp/bunny/blob/main/docs/guides/getting_started.md). ### Guides @@ -161,15 +165,15 @@ Bunny documentation guides are [under `docs/guides` in this repository](https:// Some highly relevant RabbitMQ documentation guides: - * [Connections](https://www.rabbitmq.com/connections.html) - * [Channels](https://www.rabbitmq.com/channels.html) - * [Queues](https://www.rabbitmq.com/queues.html) - * [Quorum queues](https://www.rabbitmq.com/quorum-queues.html) - * [Streams](https://rabbitmq.com/streams.html) (Bunny can perform basic operations on streams even though it does not implement the [RabbitMQ Stream protocol](https://github.com/rabbitmq/rabbitmq-server/blob/v3.10.x/deps/rabbitmq_stream/docs/PROTOCOL.adoc)) - * [Publishers](https://www.rabbitmq.com/publishers.html) - * [Consumers](https://www.rabbitmq.com/consumers.html) - * Data safety: publisher and consumer [Confirmations](https://www.rabbitmq.com/confirms.html) - * [Production Checklist](https://www.rabbitmq.com/production-checklist.html) + * [Connections](https://www.rabbitmq.com/docs/connections) + * [Channels](https://www.rabbitmq.com/docs/channels) + * [Queues](https://www.rabbitmq.com/docs/queues) + * [Quorum queues](https://www.rabbitmq.com/docs/quorum-queues) + * [Streams](https://rabbitmq.com/docs/streams) (Bunny can perform basic operations on streams even though it does not implement the [RabbitMQ Stream protocol](https://github.com/rabbitmq/rabbitmq-server/blob/v4.0.x/deps/rabbitmq_stream/docs/PROTOCOL.adoc)) + * [Publishers](https://www.rabbitmq.com/docs/publishers) + * [Consumers](https://www.rabbitmq.com/docs/consumers) + * Data safety: publisher and consumer [Confirmations](https://www.rabbitmq.com/docs/confirms) + * [Production Checklist](https://www.rabbitmq.com/docs/production-checklist) ### API Reference @@ -180,17 +184,14 @@ Some highly relevant RabbitMQ documentation guides: ### Mailing List -[Bunny has a mailing list](http://groups.google.com/group/ruby-amqp). Please use it for all questions, -investigations, and discussions. GitHub issues should be used for specific, well understood, actionable -maintainers and contributors can work on. - -We encourage you to also join the [RabbitMQ mailing list](https://groups.google.com/forum/#!forum/rabbitmq-users) -mailing list. Feel free to ask any questions that you may have. - +Please use [GitHub Discussions](https://github.com/ruby-amqp/bunny/discussions) for questions. -## Continuous Integration +GitHub issues should be used for specific, well understood, actionable +maintainers and contributors can work on. -[![Build Status](https://travis-ci.org/ruby-amqp/bunny.svg)](https://travis-ci.org/ruby-amqp/bunny/) +We encourage you to keep an eye on [RabbitMQ Discussions](https://github.com/rabbitmq/rabbitmq-server/discussions), +join the [RabbitMQ mailing list](https://groups.google.com/forum/#!forum/rabbitmq-users) +and the [RabbitMQ Discord server](https://rabbitmq.com/discord). ### Reporting Issues diff --git a/lib/bunny/channel.rb b/lib/bunny/channel.rb index 24235315..faf70901 100644 --- a/lib/bunny/channel.rb +++ b/lib/bunny/channel.rb @@ -15,11 +15,7 @@ require "bunny/return_info" require "bunny/message_properties" -if defined?(JRUBY_VERSION) - require "bunny/concurrent/linked_continuation_queue" -else - require "bunny/concurrent/continuation_queue" -end +require "bunny/concurrent/continuation_queue" module Bunny # ## Channels in RabbitMQ @@ -2113,18 +2109,10 @@ def reset_continuations @basic_get_continuations = new_continuation end - - if defined?(JRUBY_VERSION) - # @private - def new_continuation - Concurrent::LinkedContinuationQueue.new - end - else - # @private - def new_continuation - Concurrent::ContinuationQueue.new - end - end # if defined? + # @private + def new_continuation + Concurrent::ContinuationQueue.new + end # @private def guarding_against_stale_delivery_tags(tag, &block) diff --git a/lib/bunny/concurrent/linked_continuation_queue.rb b/lib/bunny/concurrent/linked_continuation_queue.rb deleted file mode 100644 index 09b8aa16..00000000 --- a/lib/bunny/concurrent/linked_continuation_queue.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -if !defined?(JRUBY_VERSION) - raise "Bunny::Concurrent::LinkedContinuationQueue can only be used on JRuby!" -end - -require "java" - -java_import java.util.concurrent.LinkedBlockingQueue -java_import java.util.concurrent.TimeUnit - -module Bunny - module Concurrent - # Continuation queue implementation for JRuby. - # - # On JRuby, we'd rather use reliable and heavily battle tested j.u.c. - # primitives with well described semantics than informally specified, clumsy - # and limited Ruby standard library parts. - # - # This is an implementation of the continuation queue on top of the linked blocking - # queue in j.u.c. - # - # Compared to the Ruby standard library Queue, there is one limitation: you cannot - # push a nil on the queue, it will fail with a null pointer exception. - # @private - class LinkedContinuationQueue - def initialize(*args, &block) - @q = LinkedBlockingQueue.new - end - - def push(el, timeout_in_ms = nil) - if timeout_in_ms - @q.offer(el, timeout_in_ms, TimeUnit::MILLISECONDS) - else - @q.offer(el) - end - end - alias << push - - def pop - @q.take - end - - def poll(timeout_in_ms = nil) - if timeout_in_ms - v = @q.poll(timeout_in_ms, TimeUnit::MILLISECONDS) - raise ::Timeout::Error.new("operation did not finish in #{timeout_in_ms} ms") if v.nil? - v - else - @q.poll - end - end - - def clear - @q.clear - end - - def method_missing(selector, *args, &block) - @q.__send__(selector, *args, &block) - end - end - end -end diff --git a/lib/bunny/jruby/socket.rb b/lib/bunny/jruby/socket.rb deleted file mode 100644 index df1e895e..00000000 --- a/lib/bunny/jruby/socket.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require "bunny/cruby/socket" - -module Bunny - module JRuby - # TCP socket extension that uses Socket#readpartial to avoid excessive CPU - # burn after some time. See issue #165. - # @private - module Socket - include Bunny::Socket - - def self.open(host, port, options = {}) - socket = ::Socket.tcp(host, port, nil, nil, - connect_timeout: options[:connect_timeout]) - if ::Socket.constants.include?('TCP_NODELAY') || ::Socket.constants.include?(:TCP_NODELAY) - socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true) - end - socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options.fetch(:keepalive, true) - socket.extend self - socket.options = { :host => host, :port => port }.merge(options) - socket - rescue Errno::ETIMEDOUT - raise ClientTimeout - end - - # Reads given number of bytes with an optional timeout - # - # @param [Integer] count How many bytes to read - # @param [Integer] timeout Timeout - # - # @return [String] Data read from the socket - # @api public - def read_fully(count, timeout = nil) - value = +'' - - begin - loop do - value << read_nonblock(count - value.bytesize) - break if value.bytesize >= count - end - rescue EOFError - # JRuby specific fix via https://github.com/jruby/jruby/issues/1694#issuecomment-54873532 - IO.select([self], nil, nil, timeout) - retry - rescue *READ_RETRY_EXCEPTION_CLASSES - if IO.select([self], nil, nil, timeout) - retry - else - raise Timeout::Error, "IO timeout when reading #{count} bytes" - end - end - - value - end # read_fully - - end - end -end diff --git a/lib/bunny/jruby/ssl_socket.rb b/lib/bunny/jruby/ssl_socket.rb deleted file mode 100644 index f295dd1c..00000000 --- a/lib/bunny/jruby/ssl_socket.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module Bunny - module JRuby - begin - require "bunny/cruby/ssl_socket" - require "openssl" - - # TLS-enabled TCP socket that implements convenience - # methods found in Bunny::Socket. - class SSLSocket < Bunny::SSLSocket - - def initialize(*args) - super - @__bunny_socket_eof_flag__ = false - end - - # Reads given number of bytes with an optional timeout - # - # @param [Integer] count How many bytes to read - # @param [Integer] timeout Timeout - # - # @return [String] Data read from the socket - # @api public - def read_fully(count, timeout = nil) - return nil if @__bunny_socket_eof_flag__ - - value = +'' - begin - loop do - value << read_nonblock(count - value.bytesize) - break if value.bytesize >= count - end - rescue EOFError => e - @__bunny_socket_eof_flag__ = true - rescue OpenSSL::SSL::SSLError => e - if e.message == "read would block" - if IO.select([self], nil, nil, timeout) - retry - else - raise Timeout::Error, "IO timeout when reading #{count} bytes" - end - else - raise e - end - rescue *READ_RETRY_EXCEPTION_CLASSES => e - if IO.select([self], nil, nil, timeout) - retry - else - raise Timeout::Error, "IO timeout when reading #{count} bytes" - end - end - value - end - end - rescue LoadError => le - puts "Could not load OpenSSL" - end - end -end diff --git a/lib/bunny/session.rb b/lib/bunny/session.rb index b5ca4398..923c7e36 100644 --- a/lib/bunny/session.rb +++ b/lib/bunny/session.rb @@ -12,11 +12,7 @@ require "bunny/authentication/plain_mechanism_encoder" require "bunny/authentication/external_mechanism_encoder" -if defined?(JRUBY_VERSION) - require "bunny/concurrent/linked_continuation_queue" -else - require "bunny/concurrent/continuation_queue" -end +require "bunny/concurrent/continuation_queue" require "amq/protocol/client" require "amq/settings" @@ -1080,16 +1076,7 @@ def maybe_shutdown_reader_loop # this is the easiest way to wait until the loop # is guaranteed to have terminated @reader_loop.terminate_with(ShutdownSignal) - # joining the thread here may take forever - # on JRuby because sun.nio.ch.KQueueArrayWrapper#kevent0 is - # a native method that cannot be (easily) interrupted. - # So we use this ugly hack or else our test suite takes forever - # to run on JRuby (a new connection is opened/closed per example). MK. - if defined?(JRUBY_VERSION) - sleep 0.075 - else - @reader_loop.join - end + @reader_loop.join else # single threaded mode, nothing to do. MK. end @@ -1413,16 +1400,9 @@ def credentials_encoder_for(mechanism) Authentication::CredentialsEncoder.for_session(self) end - if defined?(JRUBY_VERSION) - # @private - def reset_continuations - @continuations = Concurrent::LinkedContinuationQueue.new - end - else - # @private - def reset_continuations - @continuations = Concurrent::ContinuationQueue.new - end + # @private + def reset_continuations + @continuations = Concurrent::ContinuationQueue.new end # @private diff --git a/lib/bunny/socket.rb b/lib/bunny/socket.rb index 4507c520..d0b7e656 100644 --- a/lib/bunny/socket.rb +++ b/lib/bunny/socket.rb @@ -1,16 +1,9 @@ # frozen_string_literal: true -# See #165. MK. -if defined?(JRUBY_VERSION) - require "bunny/jruby/socket" +require "bunny/cruby/socket" - module Bunny - SocketImpl = JRuby::Socket - end -else - require "bunny/cruby/socket" - - module Bunny - SocketImpl = Socket - end -end +module Bunny + # An alias for the standard MRI Socket, + # exists from the days of JRuby support. + SocketImpl = Socket +end \ No newline at end of file diff --git a/lib/bunny/ssl_socket.rb b/lib/bunny/ssl_socket.rb index 94ffe984..2ed0ceee 100644 --- a/lib/bunny/ssl_socket.rb +++ b/lib/bunny/ssl_socket.rb @@ -1,16 +1,9 @@ # frozen_string_literal: true -# See #165. MK. -if defined?(JRUBY_VERSION) - require "bunny/jruby/ssl_socket" +require "bunny/cruby/ssl_socket" - module Bunny - SSLSocketImpl = JRuby::SSLSocket - end -else - require "bunny/cruby/ssl_socket" - - module Bunny - SSLSocketImpl = SSLSocket - end -end +module Bunny + # An alias for the standard SSLSocket, + # exists from the days of JRuby support. + SSLSocketImpl = SSLSocket +end \ No newline at end of file diff --git a/lib/bunny/transport.rb b/lib/bunny/transport.rb index 9549f489..704e8b6d 100644 --- a/lib/bunny/transport.rb +++ b/lib/bunny/transport.rb @@ -153,51 +153,26 @@ def configure_tls_context(&block) block.call(@tls_context) if @tls_context end - if defined?(JRUBY_VERSION) - # Writes data to the socket. - def write(data) - return write_without_timeout(data) unless @write_timeout + # Writes data to the socket. If read/write timeout was specified the operation will return after that + # amount of time has elapsed waiting for the socket. + def write(data) + return write_without_timeout(data) unless @write_timeout - begin - if open? - @writes_mutex.synchronize do - @socket.write(data) - end - end - rescue SystemCallError, Timeout::Error, Bunny::ConnectionError, IOError => e - @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})" - close - @status = :not_connected - - if @session.automatically_recover? - @session.handle_network_failure(e) - else - @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e)) + begin + if open? + @writes_mutex.synchronize do + @socket.write_nonblock_fully(data, @write_timeout) end end - end - else - # Writes data to the socket. If read/write timeout was specified the operation will return after that - # amount of time has elapsed waiting for the socket. - def write(data) - return write_without_timeout(data) unless @write_timeout + rescue SystemCallError, Timeout::Error, Bunny::ConnectionError, IOError => e + @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})" + close + @status = :not_connected - begin - if open? - @writes_mutex.synchronize do - @socket.write_nonblock_fully(data, @write_timeout) - end - end - rescue SystemCallError, Timeout::Error, Bunny::ConnectionError, IOError => e - @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})" - close - @status = :not_connected - - if @session.automatically_recover? - @session.handle_network_failure(e) - else - @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e)) - end + if @session.automatically_recover? + @session.handle_network_failure(e) + else + @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e)) end end end diff --git a/spec/unit/heartbeat_sender_spec.rb b/spec/unit/heartbeat_sender_spec.rb deleted file mode 100644 index d82e14f8..00000000 --- a/spec/unit/heartbeat_sender_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "spec_helper" - -describe Bunny::HeartbeatSender do - let(:transport) { instance_double("Bunny::Transport") } - - # let(:logger) { StringIO.new } # keep test output clear - let(:logger) { Logger.new(STDOUT) } - - let(:heartbeat_sender) do - allow(Bunny::Transport).to receive(:new).and_return(transport) - described_class.new(Bunny::Transport.new, logger) - end - - it "raises an error when standard error is raised" do - allow(logger).to receive(:error) - # This simulates a transport that raises an error. - allow(heartbeat_sender).to receive(:beat).and_raise(StandardError.new("This error should be logged")) - - heartbeat_sender.start - expect(logger).to have_received(:error).with("Error in the hearbeat sender: This error should be logged") - end -end