Skip to content

feat!: report permanent reconnect failure via OnReconnectAborted - #23

Merged
KARTIKrocks merged 1 commit into
mainfrom
feat/on-reconnect-aborted
Jul 28, 2026
Merged

feat!: report permanent reconnect failure via OnReconnectAborted#23
KARTIKrocks merged 1 commit into
mainfrom
feat/on-reconnect-aborted

Conversation

@KARTIKrocks

@KARTIKrocks KARTIKrocks commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Problem

v0.9.0 added a fail-fast gate so the reconnect loop stops instead of re-submitting
rejected credentials forever. It surfaced that terminal event by invoking
OnDisconnect a second time — but gave callers no way to tell that call apart
from the ordinary one that precedes every reconnect attempt:

  • both calls can carry an *amqp.Error, so the error type is not a signal;
  • MaxReconnectAttempts defaults to 0 (unlimited), so the ErrMaxReconnects
    sentinel never appears on the default configuration;
  • which left the auth-abort case recognizable only by re-implementing the
    unexported dialErrorIsPermanent reply-code classification (403/530) against
    amqp091-go directly.

So a caller had to pick between treating every transient blip as fatal, or never
noticing the connection was permanently dead — the exact failure the v0.9.0 gate
was built to surface.

Change

Overloading one callback with two meanings was the mistake, so this splits them
rather than papering over it with a marker error:

Callback Fires Meaning
OnDisconnect once per lost connection, before retrying briefly down, backing off
OnReconnectAborted at most once, when the loop gives up gone for good
conn.OnDisconnect(func(err error) {
    health.Degraded(err) // reconnecting, with backoff
})
conn.OnReconnectAborted(func(err error) {
    health.Fatal(err) // never coming back on its own
})

OnReconnectAborted receives the cause unwrapped: ErrMaxReconnects when the
attempt budget ran out, otherwise the rejected dial error, which errors.As
unwraps to its *amqp.Error. The callback identity is the signal, so no marker
sentinel is needed. Closing the connection yourself with Close is not an abort
and does not fire it.

Implementation notes:

  • The abort callback is read at dispatch time, not captured when the connection
    dropped. With unlimited attempts the retry loop can run for a long time, and a
    callback registered during that window must still be honored.
  • safeOnDisconnect generalizes to safeCallback(name, fn, err), naming the
    callback in its panic log. Both callbacks stay synchronous and panic-contained.

Breaking change

Code relying on OnDisconnect firing on terminal give-up must move to
OnReconnectAborted, and ErrReconnectAborted is removed. Code that only logs
disconnects needs no change. The affected behavior shipped in v0.9.0 and has no
known users.

Testing

  • go test -race unit suite green; new coverage for callback dispatch, the
    unwrapped abort causes, independent registration slots, and late registration.
  • Full integration suite green on RabbitMQ 4 and RabbitMQ 3. Both reconnect
    tests now assert both halves of the contract: the abort lands on
    OnReconnectAborted exactly once, and the connection-lost notification stays on
    OnDisconnect.
  • golangci-lint run --build-tags=integration: 0 issues.

Docs updated: README (callback table, examples, error list), package docs on both
callbacks, examples/basic/main.go, CHANGELOG (0.10.0).

Summary by CodeRabbit

  • New Features
    • Added an OnReconnectAborted callback for when automatic reconnection permanently stops.
    • Reports unrecoverable connection errors and maximum retry exhaustion through the new callback.
  • Behavior Changes
    • OnDisconnect now fires only when a connection is lost before reconnection attempts begin.
    • Applications relying on a final reconnection-stopped notification should use OnReconnectAborted.
  • Documentation
    • Updated examples and reconnection guidance to explain callback behavior and error handling.

v0.9.0 made OnDisconnect fire a second time when automatic reconnection
permanently gave up, but left callers no way to tell that call apart from
the ordinary one preceding every reconnect attempt: both can carry a
*amqp.Error, and since MaxReconnectAttempts defaults to 0 the
ErrMaxReconnects sentinel never appears, so the auth-abort case was
recognizable only by re-implementing dialErrorIsPermanent's reply-code
classification against amqp091-go directly. A caller therefore had to
choose between treating every blip as fatal and never noticing the
connection was permanently dead — the exact failure the v0.9.0 fail-fast
gate existed to surface.

Overloading one callback with two meanings was the mistake, so split them
rather than papering over it with a marker error. OnDisconnect goes back to
what its name says: fired once per lost connection, before a retry, never
terminally. The new OnReconnectAborted fires at most once, when the loop
gives up, and receives the cause unwrapped — ErrMaxReconnects, or the
rejected dial error that errors.As unwraps to its *amqp.Error. The callback
identity is the signal, so no sentinel is needed.

The abort callback is read at dispatch time rather than captured when the
connection dropped: with unlimited attempts the loop can run for a long
time, and a callback registered in that window must still be honored.

safeOnDisconnect generalizes to safeCallback, naming the callback in its
panic log. Integration tests assert both halves of the contract: the abort
lands on OnReconnectAborted exactly once, and the connection-lost
notification stays on OnDisconnect.

BREAKING CHANGE: code relying on OnDisconnect firing on terminal give-up
must move to OnReconnectAborted; ErrReconnectAborted is removed.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The connection API now exposes OnReconnectAborted for permanent reconnection failure. OnDisconnect reports connection loss before retries, while terminal causes such as authentication failure or ErrMaxReconnects use the new callback. Tests and documentation cover the updated behavior.

Changes

Reconnection callback lifecycle

Layer / File(s) Summary
Callback API and safe dispatch
rabbitmq.go, config_test.go
Adds OnReconnectAborted, generalized panic-safe callback dispatch, independent callback registration, and late callback lookup.
Reconnect-loop event routing
rabbitmq.go, config_test.go, integration_test.go
Routes transient loss to OnDisconnect and permanent authentication or retry-budget failures to OnReconnectAborted, with at-most-once assertions.
Public behavior documentation and examples
CHANGELOG.md, README.md, examples/basic/main.go
Documents the callback contract change, terminal error handling, and updated connection lifecycle logging.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Broker
  participant ReconnectLoop
  participant Callbacks
  participant Application
  Broker-->>ReconnectLoop: connection loss or dial result
  ReconnectLoop->>Callbacks: invoke OnDisconnect with loss error
  ReconnectLoop->>Broker: retry connection
  Broker-->>ReconnectLoop: permanent error or retry exhaustion
  ReconnectLoop->>Callbacks: invoke OnReconnectAborted with terminal cause
  Callbacks->>Application: deliver callback error
Loading

Possibly related PRs

  • KARTIKrocks/rabbitwrap#22: Both update reconnection error classification and terminal give-up handling in rabbitmq.go and its tests.

Poem

When connections drift and retries start,
OnDisconnect marks the transient part.
If every attempt must be discarded,
OnReconnectAborted speaks—clear-hearted.
Errors arrive, callbacks stay bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main API change: reporting permanent reconnect failure via OnReconnectAborted.
Description check ✅ Passed The description covers the problem, change, breaking change, and testing, though it doesn't follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/on-reconnect-aborted

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

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rabbitmq.go 76.92% 3 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: 1

🤖 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 `@CHANGELOG.md`:
- Around line 8-42: Update the 0.10.0 section in CHANGELOG.md by adding a
distinct “Removed” subsection and documenting the removal of the previously
public ErrReconnectAborted sentinel error as a breaking user-facing change.
🪄 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: 94a7fa4f-5499-4b3b-80f6-d25bd5f3b435

📥 Commits

Reviewing files that changed from the base of the PR and between 6b473be and e22dd37.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • README.md
  • config_test.go
  • examples/basic/main.go
  • integration_test.go
  • rabbitmq.go

Comment thread CHANGELOG.md
Comment on lines +8 to +42
## [0.10.0] - 2026-07-28

### Added

- **`Connection.OnReconnectAborted` — a dedicated callback for "the connection
is never coming back".** It fires at most once, when automatic reconnection
permanently gives up, and receives the cause: `ErrMaxReconnects` when the
attempt budget ran out, otherwise the rejected dial error, which `errors.As`
unwraps to its `*amqp.Error`. Closing the connection yourself with `Close` is
not an abort and does not fire it.

```go
conn.OnDisconnect(func(err error) {
health.Degraded(err) // reconnecting, with backoff
})
conn.OnReconnectAborted(func(err error) {
health.Fatal(err) // never coming back on its own
})
```

### Changed

- **BREAKING: `OnDisconnect` no longer doubles as the terminal notification.**
v0.9.0 invoked it a second time when reconnection gave up, which left callers
no way to tell that call apart from the ordinary one preceding every reconnect
attempt: both can carry a `*amqp.Error`, and with the default
`MaxReconnectAttempts` of 0 the `ErrMaxReconnects` sentinel never appears, so
the auth-abort case was distinguishable only by re-implementing the library's
reply-code classification against `amqp091-go` directly.

`OnDisconnect` is now exactly what its name says: fired once per lost
connection, before reconnection is attempted, never terminally. Code that
needs the terminal signal moves to `OnReconnectAborted`; code that only logs
disconnects needs no change.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing "Removed" entry for ErrReconnectAborted.

This release adds OnReconnectAborted (Added) and changes OnDisconnect semantics (Changed), but per the PR objectives ErrReconnectAborted — a previously public sentinel error — is also removed in this release. Keep a Changelog reserves a distinct ### Removed section specifically for "features or functionality that have been permanently removed," separate from Changed. Omitting it here means a reader scanning only "Removed" (or a changelog-parsing tool) could miss this breaking removal entirely.

As per coding guidelines, "Update CHANGELOG.md in Keep a Changelog format for user-facing changes."

📝 Suggested addition
+### Removed
+
+- **BREAKING: `ErrReconnectAborted` sentinel error removed.** Terminal
+  reconnect failures are now reported via `OnReconnectAborted`'s callback
+  argument (`ErrMaxReconnects` or the underlying dial error) instead of a
+  dedicated sentinel.
+
 ### Changed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## [0.10.0] - 2026-07-28
### Added
- **`Connection.OnReconnectAborted` — a dedicated callback for "the connection
is never coming back".** It fires at most once, when automatic reconnection
permanently gives up, and receives the cause: `ErrMaxReconnects` when the
attempt budget ran out, otherwise the rejected dial error, which `errors.As`
unwraps to its `*amqp.Error`. Closing the connection yourself with `Close` is
not an abort and does not fire it.
```go
conn.OnDisconnect(func(err error) {
health.Degraded(err) // reconnecting, with backoff
})
conn.OnReconnectAborted(func(err error) {
health.Fatal(err) // never coming back on its own
})
```
### Changed
- **BREAKING: `OnDisconnect` no longer doubles as the terminal notification.**
v0.9.0 invoked it a second time when reconnection gave up, which left callers
no way to tell that call apart from the ordinary one preceding every reconnect
attempt: both can carry a `*amqp.Error`, and with the default
`MaxReconnectAttempts` of 0 the `ErrMaxReconnects` sentinel never appears, so
the auth-abort case was distinguishable only by re-implementing the library's
reply-code classification against `amqp091-go` directly.
`OnDisconnect` is now exactly what its name says: fired once per lost
connection, before reconnection is attempted, never terminally. Code that
needs the terminal signal moves to `OnReconnectAborted`; code that only logs
disconnects needs no change.
## [0.10.0] - 2026-07-28
### Added
- **`Connection.OnReconnectAborted` — a dedicated callback for "the connection
is never coming back".** It fires at most once, when automatic reconnection
permanently gives up, and receives the cause: `ErrMaxReconnects` when the
attempt budget ran out, otherwise the rejected dial error, which `errors.As`
unwraps to its `*amqp.Error`. Closing the connection yourself with `Close` is
not an abort and does not fire it.
🤖 Prompt for 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.

In `@CHANGELOG.md` around lines 8 - 42, Update the 0.10.0 section in CHANGELOG.md
by adding a distinct “Removed” subsection and documenting the removal of the
previously public ErrReconnectAborted sentinel error as a breaking user-facing
change.

Source: Coding guidelines

@KARTIKrocks
KARTIKrocks merged commit 9e5f48b into main Jul 28, 2026
11 checks passed
@KARTIKrocks
KARTIKrocks deleted the feat/on-reconnect-aborted branch July 28, 2026 06:44
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