Skip to content

ROSAENG-62134: Add rate-limit backoff guard for fleet mode HTTP 429 - #264

Merged
openshift-merge-bot[bot] merged 4 commits into
openshift:masterfrom
redhat-chai-bot:fix/rate-limit-backoff-ROSAENG-62134
Aug 6, 2026
Merged

ROSAENG-62134: Add rate-limit backoff guard for fleet mode HTTP 429#264
openshift-merge-bot[bot] merged 4 commits into
openshift:masterfrom
redhat-chai-bot:fix/rate-limit-backoff-ROSAENG-62134

Conversation

@redhat-chai-bot

@redhat-chai-bot redhat-chai-bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

When the OCM service log API returns HTTP 429 (rate limit exceeded), ocm-agent fleet mode enters an infinite retry loop. The restoreNotificationStatus() function rolls back lastTransitionTime after every failed send, causing canSendNotification() to return true on every reconciliation cycle (~15 min). This generates ~24 wasted API requests per hour, per affected cluster, indefinitely.

Fixes ROSAENG-62134

Changes

1. pkg/ocm/ocm.go — Typed RateLimitError on 429

  • Added RateLimitError struct with Error() and Unwrap() methods
  • Modified SendServiceLog to return &RateLimitError{...} when the OCM API responds with HTTP 429, in both the SDK error path and the unexpected-status path

2. pkg/handlers/webhookrhobsreceiver.go — Rate-limit-aware backoff guard

  • Added in-memory sync.Map (rateLimitBackoffs) keyed by notificationName:clusterID to track when 429 was last received
  • Added rateLimitRetryInterval constant (30 minutes)
  • Pre-send guard: In processAlert(), before proceeding to the send path, checks the backoff map. If within the 30-minute window, logs and returns nil (skipping the send)
  • Error handler: Uses errors.As() to detect RateLimitError, stores time.Now() in the backoff map, and logs a warning. Existing restoreNotificationStatus() and metrics calls are preserved
  • Success path: Clears any backoff entry after a successful send

Resulting Behavior

Scenario Before After
429 received Retry every ~15 min forever Retry every ~30-45 min (backoff window)
Rate limit clears Still retrying every 15 min Next retry succeeds, 24h ResendWait kicks in
Pod restart during backoff N/A One extra failed attempt, then backoff re-established

Design Notes

  • restoreNotificationStatus() is still called on 429 — this keeps FiringNotificationSentCount accurate
  • The in-memory sync.Map is ephemeral — on pod restart one extra failed attempt occurs, then the backoff is re-established (self-healing)
  • No changes to canSendNotification() — the backoff is an early guard in processAlert, keeping concerns separated
  • No interface changes — SendServiceLog still returns error, so mock generation is unaffected

AI-generated. Review for accuracy.

@ravitri requested in Slack thread

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of OCM API rate limits.
    • Alerts are temporarily paused for affected hosted clusters after a rate-limit response, preventing repeated failed attempts.
    • Alert delivery automatically resumes after the backoff period or once a successful send clears the rate-limit state.
    • Rate-limit errors are now identified consistently across API responses and delivery failures.
    • Concurrent alert activity is handled safely to preserve the latest rate-limit status.

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Aug 4, 2026
@openshift-ci-robot

openshift-ci-robot commented Aug 4, 2026

Copy link
Copy Markdown

@redhat-chai-bot: This pull request references ROSAENG-62134 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the bug to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

When the OCM service log API returns HTTP 429 (rate limit exceeded), ocm-agent fleet mode enters an infinite retry loop. The restoreNotificationStatus() function rolls back lastTransitionTime after every failed send, causing canSendNotification() to return true on every reconciliation cycle (~15 min). This generates ~24 wasted API requests per hour, per affected cluster, indefinitely.

Fixes ROSAENG-62134

Changes

1. pkg/ocm/ocm.go — Typed RateLimitError on 429

  • Added RateLimitError struct with Error() and Unwrap() methods
  • Modified SendServiceLog to return &RateLimitError{...} when the OCM API responds with HTTP 429, in both the SDK error path and the unexpected-status path

2. pkg/handlers/webhookrhobsreceiver.go — Rate-limit-aware backoff guard

  • Added in-memory sync.Map (rateLimitBackoffs) keyed by notificationName:clusterID to track when 429 was last received
  • Added rateLimitRetryInterval constant (30 minutes)
  • Pre-send guard: In processAlert(), before proceeding to the send path, checks the backoff map. If within the 30-minute window, logs and returns nil (skipping the send)
  • Error handler: Uses errors.As() to detect RateLimitError, stores time.Now() in the backoff map, and logs a warning. Existing restoreNotificationStatus() and metrics calls are preserved
  • Success path: Clears any backoff entry after a successful send

Resulting Behavior

Scenario Before After
429 received Retry every ~15 min forever Retry every ~30-45 min (backoff window)
Rate limit clears Still retrying every 15 min Next retry succeeds, 24h ResendWait kicks in
Pod restart during backoff N/A One extra failed attempt, then backoff re-established

Design Notes

  • restoreNotificationStatus() is still called on 429 — this keeps FiringNotificationSentCount accurate
  • The in-memory sync.Map is ephemeral — on pod restart one extra failed attempt occurs, then the backoff is re-established (self-healing)
  • No changes to canSendNotification() — the backoff is an early guard in processAlert, keeping concerns separated
  • No interface changes — SendServiceLog still returns error, so mock generation is unaffected

AI-generated. Review for accuracy.

@ravitri requested in Slack thread

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested review from Tafhim and chamalabey August 4, 2026 01:43
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d1fdde50-623d-477e-8824-5754fcf59c47

📥 Commits

Reviewing files that changed from the base of the PR and between f50872a and aaad6bc.

📒 Files selected for processing (4)
  • pkg/handlers/webhookrhobsreceiver.go
  • pkg/handlers/webhookrhobsreceiver_test.go
  • pkg/ocm/ocm.go
  • pkg/ocm/ocm_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/ocm/ocm.go
  • pkg/handlers/webhookrhobsreceiver.go

Walkthrough

Changes

Rate-limit backoff

Layer / File(s) Summary
OCM rate-limit error contract
pkg/ocm/ocm.go, pkg/ocm/ocm_test.go
SendServiceLog returns RateLimitError for HTTP 429 responses. Tests verify wrapping, errors.As, and non-429 behavior.
Webhook backoff flow
pkg/handlers/webhookrhobsreceiver.go
The receiver tracks backoff per notification and hosted cluster, skips firing alerts for 30 minutes, clears resolved alerts, and preserves newer concurrent failures.
Backoff behavior validation
pkg/handlers/webhookrhobsreceiver_test.go
Tests cover backoff creation, active-window skipping, retry after expiration, cleanup, and concurrent timestamp preservation.

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

Sequence Diagram(s)

sequenceDiagram
  participant WebhookReceiver
  participant SendServiceLog
  participant OCMAPI
  participant BackoffStore

  WebhookReceiver->>BackoffStore: Check notification and cluster backoff
  WebhookReceiver->>SendServiceLog: Send firing notification
  SendServiceLog->>OCMAPI: Submit service log
  OCMAPI-->>SendServiceLog: Return HTTP 429 or success
  SendServiceLog-->>WebhookReceiver: Return RateLimitError or success
  WebhookReceiver->>BackoffStore: Record or clear backoff
Loading

Possibly related issues

  • openshift/ocm-agent-operator#319 — The PR adds typed HTTP 429 detection and per-notification/cluster backoff handling described by the issue.

Suggested reviewers: tafhim, chamalabey

🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Structure And Quality ⚠️ Warning The added Ginkgo tests use assertions without diagnostic messages, including Expect(err) and map-state checks; no assertion-message examples exist in the repository. Add meaningful failure messages to each new Expect assertion, such as identifying the expected backoff state or HTTP status.
✅ Passed checks (14 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the added rate-limit backoff guard for fleet mode HTTP 429 responses.
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.
Stable And Deterministic Test Names ✅ Passed All added Ginkgo titles are literal, descriptive strings; the PR diff contains no interpolated, timestamped, generated, or environment-specific test-name values.
Microshift Test Compatibility ✅ Passed The PR adds only pkg unit tests; no test/e2e files changed, and the new Ginkgo tests use mocked clients and HTTP servers without unsupported MicroShift APIs or assumptions.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The PR adds only package-level Ginkgo unit tests; no test/e2e files changed. The tests use mocks and in-memory state and make no node or HA assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed The changed code adds webhook rate-limit state and tests only; no manifests, replica logic, affinity, topology spread, node selectors, tolerations, or PDB scheduling constraints were introduced.
Ote Binary Stdout Contract ✅ Passed The PR adds only logrus warnings in handler code; logrus defaults to os.Stderr, and the PR diff adds no fmt.Print, klog, or stdout writes in OTE process-level setup.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new e2e tests were added; the new Ginkgo cases are pkg unit tests using mocks and a local ghttp server, with no hardcoded IPv4 values or external connectivity.
No-Weak-Crypto ✅ Passed The full PR diff adds HTTP 429 handling, backoff state, and tests only; it introduces no weak crypto, custom cryptography, or secret/token comparisons.
Container-Privileges ✅ Passed The PR changes only Go source and tests. It adds no manifests or privilege-related settings; existing templates disable privilege escalation and drop all capabilities.
No-Sensitive-Data-In-Logs ✅ Passed New warnings log only the notification name and fixed rate-limit text; no credentials, tokens, cluster IDs, alert payloads, or request/response bodies are logged.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

… 429

When the OCM API responds with HTTP 429 (Too Many Requests) during
service log sends, ocm-agent was retrying indefinitely on every
AlertManager webhook delivery, creating a retry storm.

This change:
- Adds a RateLimitError type in pkg/ocm that wraps 429 responses from
  SendServiceLog, allowing callers to distinguish rate limits from
  other errors.
- Adds an in-memory per-notification:cluster backoff map in the RHOBS
  webhook handler. On a 429 response the timestamp is recorded and
  subsequent firing alerts for that pair are silently skipped for 30
  minutes, breaking the retry loop.
- Clears the backoff entry on a successful send so normal operation
  resumes immediately once the rate limit lifts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@redhat-chai-bot
redhat-chai-bot force-pushed the fix/rate-limit-backoff-ROSAENG-62134 branch from d7f6acc to 4d92ada Compare August 4, 2026 01:48

@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: 3

🧹 Nitpick comments (1)
pkg/handlers/webhookrhobsreceiver.go (1)

46-51: 🩺 Stability & Availability | 🔵 Trivial

Confirm the deployment scope for rateLimitBackoffs.

rateLimitBackoffs is process-local. If the receiver runs with multiple replicas, a 429 under notification:clusterID in one replica will not suppress retries in another. Confirm single-replica scope is intended, or make the backoff shared.

🤖 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 `@pkg/handlers/webhookrhobsreceiver.go` around lines 46 - 51, Confirm that the
process-local rateLimitBackoffs in the webhook receiver is only intended for
single-replica deployments; if multiple replicas are supported, replace the
sync.Map-based tracking with shared backoff storage so a 429 for the same
notification:clusterID suppresses retries across replicas.
🤖 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 `@pkg/handlers/webhookrhobsreceiver.go`:
- Around line 396-399: Remove the "hosted_cluster_id" field from the Warn
logging calls around the rate-limit backoff handling, including both occurrences
associated with the alert notification. Preserve the notification name and
existing warning message while ensuring no customer resource identifier is
logged.
- Around line 391-403: Protect the entire rate-limit backoff state transition
with one shared mutex: the expired-entry check/deletion in processAlert, the 429
timestamp recording, and the successful-request cleanup. Update the logic around
rateLimitBackoffs and the related branches near the existing cleanup so a
request cannot delete or clear a newer timestamp stored by another request;
alternatively, condition cleanup on removing the exact loaded value.

In `@pkg/ocm/ocm.go`:
- Around line 258-260: Update the HTTP 429 branch in the OCM send flow to
preserve the original send error for errors.As traversal. Return a
RateLimitError whose Err is the original err without replacing it with a
non-wrapping fmt.Errorf, while retaining the existing rate-limit detection.

---

Nitpick comments:
In `@pkg/handlers/webhookrhobsreceiver.go`:
- Around line 46-51: Confirm that the process-local rateLimitBackoffs in the
webhook receiver is only intended for single-replica deployments; if multiple
replicas are supported, replace the sync.Map-based tracking with shared backoff
storage so a 429 for the same notification:clusterID suppresses retries across
replicas.
🪄 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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 05fd039e-21d8-439e-a7d7-8bc39e52cfd3

📥 Commits

Reviewing files that changed from the base of the PR and between f50872a and d7f6acc.

📒 Files selected for processing (2)
  • pkg/handlers/webhookrhobsreceiver.go
  • pkg/ocm/ocm.go

Comment thread pkg/handlers/webhookrhobsreceiver.go
Comment thread pkg/handlers/webhookrhobsreceiver.go
Comment thread pkg/ocm/ocm.go
@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.28%. Comparing base (f50872a) to head (aaad6bc).

Files with missing lines Patch % Lines
pkg/ocm/ocm.go 75.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #264      +/-   ##
==========================================
+ Coverage   55.67%   56.28%   +0.60%     
==========================================
  Files          23       23              
  Lines        1895     1926      +31     
==========================================
+ Hits         1055     1084      +29     
- Misses        785      786       +1     
- Partials       55       56       +1     
Files with missing lines Coverage Δ
pkg/handlers/webhookrhobsreceiver.go 91.73% <100.00%> (+0.82%) ⬆️
pkg/ocm/ocm.go 89.03% <75.00%> (-0.77%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds unit tests for both the RateLimitError type and the rate-limit
backoff behavior in the fleet webhook handler:

- RateLimitError: Error(), Unwrap(), errors.As() traversal
- SendServiceLog: returns *RateLimitError on 429, plain error on 500
- processAlert: backoff map populated on 429, subsequent alerts
  skipped within window, normal operation after expiry, entry cleared
  on success

Also addresses CodeRabbit review feedback:
- Remove hosted_cluster_id from warning logs (customer data concern)
- Use %w instead of %v to preserve original error in RateLimitError
- Remove eager Delete of expired entries (race-safe: stale expired
  entries are harmless since the pre-send check only blocks within
  the 30-minute window)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ravitri

ravitri commented Aug 4, 2026

Copy link
Copy Markdown
Member

Addresses openshift/ocm-agent-operator#319

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@redhat-chai-bot

Copy link
Copy Markdown
Contributor Author

/retest ci/prow/images


AI-generated. Review for accuracy.

@redhat-chai-bot

Copy link
Copy Markdown
Contributor Author

/retest pull-ci-openshift-ocm-agent-master-images


AI-generated. Review for accuracy.

@redhat-chai-bot

Copy link
Copy Markdown
Contributor Author

/test images


AI-generated. Review for accuracy.

return nil
}

// Skip firing alerts that are within the rate-limit backoff window.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The rateLimitBackoffs map entry for a notification:clusterID pair is only cleared on a successful firing send (rateLimitBackoffs.Delete in the success path). If the alert auto-resolves while the entry is in the backoff window, the resolved webhook passes through processAlert with isCurrentlyFiring=false and never touches the map.

Clear the backoff entry when a resolved alert is received for the same key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit aaad6bc.

When a resolved alert is received (isCurrentlyFiring=false), the backoff entry for that notificationName:clusterID pair is now cleared from the rateLimitBackoffs map:

if !isCurrentlyFiring {
    rateLimitKey := alert.Labels[AMLabelTemplateName] + ":" + alert.Labels[AMLabelAlertHCID]
    rateLimitBackoffs.Delete(rateLimitKey)
}

This ensures that if an alert auto-resolves while rate-limited, the stale backoff entry doesn't persist and suppress the first send attempt when the alert fires again.

Added a test: "clears backoff entry when a resolved alert is received" — seeds a backoff entry, processes a resolved alert, and verifies the entry was deleted.


AI-generated. Review for accuracy.


AI-generated. Review for accuracy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@redhat-chai-bot

Copy link
Copy Markdown
Contributor Author

/test lint


AI-generated. Review for accuracy.

@openshift-ci

openshift-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@redhat-chai-bot: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@TheUndeadKing

Copy link
Copy Markdown
Member

/label tide/merge-method-squash
/lgtm

@openshift-ci openshift-ci Bot added the tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges. label Aug 6, 2026
@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Aug 6, 2026
@TheUndeadKing

Copy link
Copy Markdown
Member

/approve

@openshift-ci

openshift-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: redhat-chai-bot, TheUndeadKing

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Aug 6, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit 8ba784b into openshift:master Aug 6, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. lgtm Indicates that a PR is ready to be merged. tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants