Skip to content

feat: declare exchanges declaratively with WithExchangeConfig - #24

Merged
KARTIKrocks merged 3 commits into
mainfrom
feat/declarative-exchange
Aug 1, 2026
Merged

feat: declare exchanges declaratively with WithExchangeConfig#24
KARTIKrocks merged 3 commits into
mainfrom
feat/declarative-exchange

Conversation

@KARTIKrocks

@KARTIKrocks KARTIKrocks commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Consumers and publishers can now declare exchanges as part of their configuration; the exchanges are declared on every channel setup, initially and after each reconnect, and for consumers before the queue and its bindings.

This closes a cold-start hazard. WithBinding (and BindQueue) required the exchange to already exist: binding to a missing exchange fails with NOT_FOUND, and because that is a channel-level exception the broker closes the channel, so a consumer that started before whichever service owned the exchange could not bind at all. Declaring the exchange as part of the consumer's own topology removes the ordering requirement, on first start and on every reconnect.

NewConsumer and NewPublisher reject an exchange config with an empty name (ErrInvalidConfig) rather than attempting to declare the default exchange, which the broker refuses. Consumer.DeclareExchange now defaults an unset type to direct, and the imperative declare/bind helpers document that a failed call closes the channel they share with consuming/publishing.

Motivation

Fixes #

Changes

Checklist

  • fmt, vet, lint, test, build passes (make all)
  • New code has tests where appropriate
  • Breaking changes are documented

Summary by CodeRabbit

  • New Features

    • Added declarative exchange configuration for consumers and publishers.
    • Exchanges are automatically declared during initial setup and restored after reconnects.
    • Added support for configuring multiple exchanges and validating exchange names.
    • Unset exchange types now default to direct exchanges.
    • Added documentation and examples for exchange setup, bindings, and reconnect behavior.
  • Bug Fixes

    • Improved handling of failed exchange declarations and missing exchanges during binding setup.

Consumers and publishers can now declare exchanges as part of their
configuration; the exchanges are declared on every channel setup, initially
and after each reconnect, and for consumers before the queue and its bindings.

This closes a cold-start hazard. WithBinding (and BindQueue) required the
exchange to already exist: binding to a missing exchange fails with NOT_FOUND,
and because that is a channel-level exception the broker closes the channel,
so a consumer that started before whichever service owned the exchange could
not bind at all. Declaring the exchange as part of the consumer's own topology
removes the ordering requirement, on first start and on every reconnect.

NewConsumer and NewPublisher reject an exchange config with an empty name
(ErrInvalidConfig) rather than attempting to declare the default exchange,
which the broker refuses. Consumer.DeclareExchange now defaults an unset type
to direct, and the imperative declare/bind helpers document that a failed call
closes the channel they share with consuming/publishing.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@KARTIKrocks, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a502d694-b4e1-4e31-8dd1-7fe185bbfee3

📥 Commits

Reviewing files that changed from the base of the PR and between 23a98e3 and 3aa4b92.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • consumer.go
  • integration_test.go
  • publisher.go

Walkthrough

The library adds declarative exchange configuration for consumers and publishers. Configured exchanges are validated and declared before dependent topology during initial setup and reconnects. Tests and documentation cover declaration order, recovery, validation, and imperative channel failures.

Changes

Declarative exchange topology

Layer / File(s) Summary
Exchange configuration contracts
consumer.go, publisher.go, consumer_test.go
Consumer and publisher configurations now store multiple exchange declarations. WithExchangeConfig copies slices before appending. Empty exchange names return ErrInvalidConfig. Unset exchange types default to direct.
Consumer topology setup
consumer.go, integration_test.go, doc.go, README.md
Consumers declare exchanges before dead-letter topology, queues, and bindings during startup and reconnects. Integration tests cover cold start, exchange recovery, and missing-exchange failures.
Publisher topology setup
publisher.go, examples/publisher/main.go, integration_test.go, README.md, CHANGELOG.md
Publishers declare configured exchanges during setup and reconnects. The example uses declarative configuration. Integration tests verify recovery and passive exchange polling. Documentation describes the new behavior and imperative declaration semantics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

Exchanges rise before queues bind,
Reconnects restore the topology line.
Publishers declare, consumers agree,
Direct defaults flow cleanly.
RabbitMQ channels keep the beat.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: declarative exchange configuration through WithExchangeConfig.
Description check ✅ Passed The description covers the summary, motivation, and technical changes, but the Changes list, issue reference, and checklist remain incomplete.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/declarative-exchange

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 29.57746% with 50 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
publisher.go 19.51% 33 Missing ⚠️
consumer.go 43.33% 17 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@consumer.go`:
- Around line 1141-1185: Document on the public ExchangeConfig.Type field that
an empty value defaults to the direct exchange type, and update
WithExchangeConfig documentation if needed to expose the same behavior. Also add
integration coverage in integration_test.go for an unset Type, verifying the
broker accepts the declaration as a direct exchange.

In `@publisher.go`:
- Around line 172-179: Update Publisher.handleReconnect to retry publisher setup
after setupChannel fails, including failures from declareExchange, instead of
waiting only for the next reconnectCh signal; preserve the existing reconnect
flow and ensure retries do not proceed without a successfully established
channel.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bf9e2461-a366-4557-8c8b-1095207a92de

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5f48b and 23a98e3.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • consumer.go
  • consumer_test.go
  • doc.go
  • examples/publisher/main.go
  • integration_test.go
  • publisher.go

Comment thread consumer.go
Comment thread publisher.go
Addresses PR review feedback.

handleReconnect now retries setupChannel on a timer instead of waiting for
the next reconnect signal. Setup can fail while the connection is healthy —
a configured exchange may not be declarable yet, or may conflict with an
existing one — and no further signal is coming in that case, so a single
failed attempt left the publisher without a usable channel indefinitely.
Retries reuse the reconnect goroutine and only ever install a channel that
completed setup; a setup finishing after Close now discards its channel
rather than leaking it, mirroring the consumer.

ExchangeConfig.Type documents that an empty value declares a direct
exchange, the AMQP default type, which declareExchange has relied on since
it was introduced but only stated on the unexported helper.

Integration coverage for both: an untyped ExchangeConfig is accepted by the
broker and routes with direct semantics, and a publisher recovers on its own
after a failing declaration stops conflicting.
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds declarative exchange declaration (WithExchangeConfig) for both consumers and publishers, resolving a cold-start ordering hazard where binding to a not-yet-declared exchange would fail with NOT_FOUND and close the channel. It also adds a timer-based retry to the publisher's handleReconnect loop so a failed channel setup (e.g., conflicting exchange type) recovers automatically without waiting for the next connection loss.

  • WithExchangeConfig appends to a copied slice on both ConsumerConfig and PublisherConfig, ensuring config copies never share backing arrays; NewConsumer and NewPublisher both reject an empty exchange name early with ErrInvalidConfig.
  • declareExchange helper consolidates the declaration logic (defaults empty Type to direct) and is used by both the declarative topology path and the existing imperative DeclareExchange methods.
  • Publisher handleReconnect is refactored from a for range loop to a select-on-two-channels loop, adding a setupRetryDelay timer arm so repeated setup failures (channel-level exceptions that don't cause a connection loss) are retried on a 5s interval rather than blocking indefinitely.

Confidence Score: 5/5

Safe to merge — the changes are well-scoped, correctly handle the Close/retry race, and are backed by both unit and integration tests.

The core logic — copy-before-append slice semantics, early validation of exchange names, declaring exchanges in applyTopology before queue/bindings, the p.closed guard in setupChannel, and the nil-channel select arm in handleReconnect — is all correct. The timer-based retry in handleReconnect cleanly handles connection-healthy / channel-broken scenarios without leaking goroutines or dropping close signals. Integration tests cover cold-start, reconnect recovery, type defaulting, conflict retry, and the failure baseline.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
publisher.go Adds Exchanges slice to PublisherConfig with WithExchangeConfig (copy-before-append), declares configured exchanges in setupChannel(), and refactors handleReconnect to retry on a timer (via a nil-channel select arm) when setup fails. The p.closed check inside setupChannel() correctly handles the race between Close() and an in-flight timer retry.
consumer.go Adds Exchanges slice to ConsumerConfig with WithExchangeConfig (copy-before-append); applyTopology now declares configured exchanges before the queue/bindings; declareExchange helper defaults empty Type to ExchangeDirect; validateExchanges rejects empty names at construction time.
consumer_test.go Adds unit tests for WithExchangeConfig copy semantics, validateExchanges, and construction-time rejection of unnamed exchanges for both consumer and publisher.
integration_test.go Adds six integration tests covering cold-start, reconnect recovery, unset-type defaulting, missing-exchange failure, and publisher setup-retry; helper functions waitForExchange and declareExchangeRaw are well-scoped.
CHANGELOG.md New 0.11.0 entry documents WithExchangeConfig, the DeclareExchange type-defaulting fix, PublisherConfig comparability change, and the publisher setup-retry fix, all in proper Keep-a-Changelog format.
examples/publisher/main.go Example updated to use declarative WithExchangeConfig instead of the imperative DeclareExchange call after construction.
doc.go Package-level doc adds a 'Declarative topology' section showing WithExchangeConfig usage and explaining the cold-start hazard it solves.
README.md Updates the declarative topology section with WithExchangeConfig examples for both consumer and publisher, and adds a callout warning about imperative declare/bind sharing the consuming channel.

Sequence Diagram

sequenceDiagram
    participant App
    participant Publisher
    participant handleReconnect
    participant Connection
    participant Broker

    App->>Publisher: NewPublisher(conn, cfg.WithExchangeConfig(...))
    Publisher->>Publisher: validateExchanges(cfg.Exchanges)
    Publisher->>Publisher: setupChannel()
    Publisher->>Connection: conn.Channel()
    Publisher->>Broker: ExchangeDeclare("events", "topic", ...)
    Broker-->>Publisher: OK (idempotent)
    Publisher->>Publisher: install p.channel
    Publisher->>handleReconnect: go handleReconnect()

    Note over handleReconnect: Waits on reconnectCh or retry timer

    Broker--xConnection: connection drop
    Connection->>Connection: reconnect with backoff
    Connection->>handleReconnect: reconnectCh signal
    handleReconnect->>Publisher: setupChannel()
    Publisher->>Broker: ExchangeDeclare("events", "topic", ...)
    alt Exchange conflict (PRECONDITION_FAILED)
        Broker--xPublisher: channel closed
        Publisher-->>handleReconnect: error
        handleReconnect->>handleReconnect: "retry = time.After(5s)"
        Note over handleReconnect: waits 5s, then retries
        handleReconnect->>Publisher: setupChannel() again
        Publisher->>Broker: ExchangeDeclare("events", "topic", ...)
        Broker-->>Publisher: OK
    else Successful setup
        Broker-->>Publisher: OK
    end
    Publisher->>Publisher: install p.channel
    handleReconnect-->>handleReconnect: log channel re-established
Loading

Reviews (2): Last reviewed commit: "fix: distinguish retry from reconnect in..." | Re-trigger Greptile

Comment thread publisher.go Outdated
A timer-driven retry and a genuine reconnect signal both logged
"re-establishing channel after reconnect", so logs could not show whether an
attempt followed a connection recovery or a previous setup failure. Record
which select arm woke the loop and log accordingly.
@KARTIKrocks
KARTIKrocks merged commit 0d696f4 into main Aug 1, 2026
12 checks passed
@KARTIKrocks
KARTIKrocks deleted the feat/declarative-exchange branch August 1, 2026 10:54
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.

2 participants