Skip to content

Add broker_default with nil - #124

Open
skunkworker wants to merge 7 commits into
masterfrom
aug6_add_broker_default_queue_nil
Open

Add broker_default with nil#124
skunkworker wants to merge 7 commits into
masterfrom
aug6_add_broker_default_queue_nil

Conversation

@skunkworker

@skunkworker skunkworker commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a first party queue_type setting so queues can be declared as classic, quorum, or stream — or, by default, with no opinion at all so the broker's own default_queue_type applies.

Configurable globally or per route:

::ActionSubscriber.configure do |config|
  config.queue_type = :quorum
end

::ActionSubscriber.draw_routes do
  route UserSubscriber, :created, :queue_type => :quorum
  route AuditSubscriber, :created, :queue_type => :broker_default
end
Value x-queue-type sent
nil (default), or :broker_default not sent — broker applies its own default_queue_type
:classic classic
:quorum quorum
:stream stream

nil is the canonical "defer to the broker" value; :broker_default is accepted as a more readable spelling and normalizes to nil. Values are normalized on assignment, so a typo raises an ArgumentError where it was set rather than at route-draw time or inside MessageRetry at runtime. :quorum and :stream force durable => true, since RabbitMQ only supports those as durable queues.

The bug this uncovered

The two drivers have been disagreeing about queue type this whole time.

# march_hare 4.8:  @options.fetch(:type, args.fetch(QUEUE_TYPE, Types::CLASSIC)).to_s
# bunny 2.24:      @options[:type]

fetch only falls back when the key is absent. Since setup_queue never passed :type, JRuby has been declaring every queue with x-queue-type: classic while MRI sent no argument at all. Verified against a real MarchHare::Queue:

{}                            -> args={"x-queue-type" => "classic"}
{type: nil}                   -> args={}

Passing :type => nil explicitly — key present, value nil — is what suppresses the argument. That is the whole mechanism behind :broker_default, and it's why the option has to reach the driver as an explicit nil rather than by omitting it.

Worth noting for reviewers: you cannot send x-queue-type with a null value as a middle ground. amq-protocol will encode it as AMQP void ("\x00\x00\x00\x0E\fx-queue-typeV"), but it can't decode its own output, and RabbitMQ resolves the type via rabbit_queue_type:discover/1, which matches no known type for a void and fails the declare. Omitting the key is the only way to defer to the broker.

⚠️ Breaking on JRuby

MRI behavior is unchanged. On JRuby, newly declared queues change from classic to whatever the broker defaults to.

This is not limited to new queues. Because queue type is fixed at declaration, an existing classic queue is now redeclared without x-queue-type. That's harmless on a vhost whose default_queue_type is classic, since the broker resolves to the same type — but it fails with PRECONDITION_FAILED on a vhost defaulting to quorum or stream.

Before rolling this out to a JRuby deployment, audit every vhost it connects to:

rabbitmqctl list_vhosts name default_queue_type

If any are non-classic, set config.queue_type = :classic before deploying — that restores the previous JRuby behavior exactly.

Known limitation

MessageRetry declares its *.retry_* queues from the global config.queue_type, not the type of the route that produced the message, so a route-level type is not propagated to its retry queue. Documented in the README rather than fixed here — propagating it isn't a straight pass-through, since retry queues carry x-message-ttl and x-dead-letter-exchange and streams support neither. Happy to take it in a follow-up.

Testing

Full suite green on JRuby 10.0.4 against a live broker: 130 examples, 0 failures (1 pending is pre-existing). New coverage in spec/lib/action_subscriber/queue_type_spec.rb includes JRuby-only examples that assert the actual MarchHare::Queue argument table for each option — including one pinning the surprising omitted :type -> classic behavior, so a driver change that alters it fails loudly.

Note on commit scope

This branch carries two commits. d08277b (Appraisal matrix, CI split by Rails version, RabbitMQ spec helper) is largely pre-existing work that was already in the working tree; the only new part is giving Rails 8.0/8.1 their own CI matrix instead of exclude entries against the Ruby 3.1-class images — job coverage is unchanged at 20. 1a348ec is the queue_type feature. Review the second commit for the substance here.

🤖 Generated with Claude Code

skunkworker and others added 7 commits August 6, 2026 10:11
Introduce an Appraisal matrix covering Rails 6.1 through 8.1, with the
default-gem shims (logger, mutex_m, bigdecimal, drb, base64, benchmark)
that ActiveSupport < 7.1 needs on Ruby >= 3.4.

CircleCI now runs build_and_test as a parameterized job across the Ruby
and JRuby images and each appraisal gemfile. Rails 8.0/8.1 require Ruby
>= 3.2, so they get their own matrix (cimg/ruby:3.4, jruby:10.0) rather
than exclude entries against the Ruby 3.1-class images. Coverage is
unchanged at 20 jobs.

Also add a spec helper that waits for RabbitMQ before the integration
suite starts, so a cold broker fails with one clear message instead of a
flurry of reconnect warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a queue_type setting, configurable globally (config.queue_type) or
per route (:queue_type => ...). Values are nil (default), :classic,
:quorum and :stream, with :broker_default accepted as a readable alias
for nil. nil leaves x-queue-type off the wire so RabbitMQ applies its own
default_queue_type. :quorum and :stream force the route to be durable,
since RabbitMQ only supports those as durable queues.

Values are normalized on assignment, so an invalid value raises where it
was set rather than at route-draw time or inside MessageRetry at runtime.

BREAKING on JRuby. The two drivers disagreed: march_hare reads its :type
option with fetch(:type, ... Types::CLASSIC), and fetch only falls back
when the key is absent, so omitting :type injected x-queue-type: classic
on every declare. bunny reads @options[:type] and sent no argument at
all. Both drivers are now passed :type explicitly, so neither sends
x-queue-type by default.

MRI behavior is unchanged. On JRuby, newly declared queues change from
classic to whatever the broker defaults to. Set config.queue_type to
:classic to retain the previous behavior. Because queue type is fixed at
declaration, redeclaring an existing queue against a vhost whose
default_queue_type is not classic will fail with PRECONDITION_FAILED --
audit vhosts before upgrading a JRuby deployment.

Known limitation, documented in the README: MessageRetry declares retry
queues from the global config.queue_type rather than the originating
route's, so a route-level type is not propagated to its retry queue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump VERSION to 7.5.0 and date the changelog entry for the queue_type
setting and the JRuby x-queue-type behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit bumped to 7.5.0, which skipped the 6.x line and read
as a minor bump. Use 6.0.0 instead: a major bump is warranted because the
x-queue-type change is breaking for JRuby consumers, where queues that
march_hare previously declared as classic are now declared with whatever
the broker defaults to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gemfiles/ directory is fully derived from the Appraisals file, so
committing the stubs meant keeping generated output in sync by hand.
Gitignore the whole directory and regenerate it in CI instead.

The generate step has to override BUNDLE_GEMFILE to the root Gemfile:
the job sets it to the target appraisal gemfile, which does not exist
until this step writes it. Verified that `appraisal generate` runs from
a clean checkout with no prior bundle install, and that its output is
byte-identical to the stubs being removed here.

Cache keys now also checksum Appraisals, so editing it busts the bundle
cache, and are bumped to v3 since the previous caches predate this.

Document the local workflow in the README, since gemfiles/ no longer
exists after a fresh clone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generate step ran `appraisal generate` without installing the root
bundle first. appraisal runs under bundler, so it aborted with:

  Could not find gem 'active_publisher (= 1.6.0)' in locally installed
  gems. Run `bundle install --gemfile Gemfile` to install missing gems.

Install the root Gemfile before generating. The two bundles differ only
in their Rails pins and share vendor/bundle, so the follow-up install is
mostly a no-op.

Also cache ./gemfiles/vendor/bundle alongside ./vendor/bundle. Because
`bundle config --local` resolves relative to the directory holding
BUNDLE_GEMFILE, the appraisal bundle installs under gemfiles/, so caching
only ./vendor/bundle reinstalled the gems the tests actually use on every
run. Cache keys bumped to v4.

Verified the full sequence from a clean clone with an isolated GEM_HOME
under JRuby 10: root install, generate, and the rails_8.0 target install
all exit 0 and resolve activesupport 8.0.5.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant