Skip to content

🐛 Fix catalogd ha readiness - #2674

Merged
openshift-merge-bot[bot] merged 2 commits into
operator-framework:mainfrom
tmshort:fix-catalogd-ha-readiness
Apr 27, 2026
Merged

🐛 Fix catalogd ha readiness#2674
openshift-merge-bot[bot] merged 2 commits into
operator-framework:mainfrom
tmshort:fix-catalogd-ha-readiness

Conversation

@tmshort

@tmshorttmshort commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Fixes catalogd to permit multiple replicas. Adds tests to the experimental-e2e where there are multiple nodes, and subsequently, multiple replicas. This is part of a fix to avoid OLMv1 becoming unready when a cluster is upgraded.

Related to: OCPBUGS-62517

catalogd's catalog HTTP server previously called net.Listen eagerly at startup on every pod, even non-leaders that never called http.Serve. With replicas > 1 this caused ~50% of catalog requests to queue indefinitely in the kernel accept backlog.

Fix: replace manager.Server with a custom catalogServerRunnable that binds the port lazily inside Start() (only called on the leader) and closes a channel to signal readiness. A /readyz check selects on that channel, so non-leader pods fail the probe and are excluded from Service endpoints. cmd/catalogd/main.go health/readiness setup is now identical to cmd/operator-controller/main.go.

With that fix in place, helm/experimental.yaml is updated to set replicas: 2 for both components so the experimental (2-node kind) e2e suite exercises the multi-replica path. A new @CatalogdHA scenario force-deletes the catalogd leader pod and asserts that a new leader is elected and the catalog resumes serving. The scenario is automatically skipped in the standard 1-node suite (gated via BeforeSuite node-count detection in featureGates). The experimental e2e timeout is bumped from 20m to 25m to accommodate worst-case leader re-election (~163s).

Description

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

CopilotAI review requested due to automatic review settings April 24, 2026 19:34
@netlify

netlifyBot commented Apr 24, 2026

Copy link
Copy Markdown

Deploy Preview for olmv1 ready!

NameLink
🔨 Latest commitc74adff
🔍 Latest deploy loghttps://app.netlify.com/projects/olmv1/deploys/69efae48ebd3f000084e826e
😎 Deploy Previewhttps://deploy-preview-2674--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes catalogd HA behavior and adds an experimental e2e scenario to exercise multi-replica failover, aiming to prevent catalog serving stalls/unreadiness during upgrades.

Changes:

  • Introduces HA-gated e2e steps + a new @CatalogdHA feature scenario that deletes the catalogd leader pod and waits for leader re-election.
  • Updates experimental manifests/helm values to run catalogd and operator-controller with 2 replicas.
  • Reworks catalogd HTTP server integration to use a custom runnable + readiness check.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
internal/catalogd/serverutil/serverutil.goReplaces controller-runtime manager.Server usage with a custom runnable and a readyz check.
test/e2e/steps/steps.goRegisters new Godog steps for HA leader failover scenario.
test/e2e/steps/hooks.goAdds a CatalogdHA feature gate and enables it based on node count.
test/e2e/steps/ha_steps.goImplements steps to force-delete the leader pod and detect a newly elected leader.
test/e2e/features/ha.featureAdds @CatalogdHA scenario validating catalog continues serving after leader disruption.
manifests/experimental.yamlSets catalogd and operator-controller replicas to 2 for experimental installs.
manifests/experimental-e2e.yamlSets catalogd and operator-controller replicas to 2 for experimental-e2e installs.
helm/experimental.yamlSets Helm experimental values to deploy both components with 2 replicas.
MakefileIncreases default e2e timeout and bumps experimental-e2e timeout to 25m.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadMakefile
Comment threadtest/e2e/features/ha.feature Outdated
Comment threadinternal/catalogd/serverutil/serverutil.go
Comment threadinternal/catalogd/serverutil/serverutil.go
Comment threadtest/e2e/steps/hooks.go Outdated
@tmshort
tmshortforce-pushed the fix-catalogd-ha-readiness branch from 8c2f948 to 99db2dcCompareApril 24, 2026 20:03
@codecov

codecovBot commented Apr 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.74419% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.96%. Comparing base (0e70fe3) to head (c74adff).
⚠️ Report is 1 commits behind head on main.

Files with missing linesPatch %Lines
internal/catalogd/serverutil/serverutil.go76.19%6 Missing and 4 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2674 +/- ##
==========================================
- Coverage 68.03% 67.96% -0.07% 
==========================================
Files 144 144 Lines 10573 10595 +22 ==========================================
+ Hits 7193 7201 +8 - Misses 2863 2871 +8 - Partials 517 523 +6 
FlagCoverage Δ
e2e37.44% <74.41%> (+0.05%)⬆️
experimental-e2e52.81% <76.74%> (+0.16%)⬆️
unit53.50% <2.32%> (-0.14%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dtfranz

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-ciopenshift-ciBot added the lgtm Indicates that a PR is ready to be merged. label Apr 27, 2026
@openshift-ciopenshift-ciBot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Apr 27, 2026

@rashmigottipatirashmigottipati left a comment

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.

/lgtm

@openshift-ci

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: camilamacedo86, rashmigottipati

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

@camilamacedo86

Copy link
Copy Markdown
Contributor

@tmshort we will need to rebase

@tmshort

tmshort commented Apr 27, 2026

Copy link
Copy Markdown
ContributorAuthor

/hold
Want to test this for downstream compatibility.

@openshift-ciopenshift-ciBot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Apr 27, 2026
The catalog HTTP server has OnlyServeWhenLeader: true, so only the leader
pod should serve catalog content. Previously, net.Listen was called eagerly
at startup for all pods: the listen socket was bound on non-leaders even
though http.Serve was never called, causing TCP connections to queue without
being served. With replicas > 1 this made ~50% of catalog content requests
fail silently.
Replace manager.Server with a custom Runnable (catalogServerRunnable) in
serverutil that:
- Binds the catalog port lazily inside Start(), which is only called on the
leader by controller-runtime's leader election machinery.
- Closes a ready channel once the listener is established, and registers a
channel-select readiness check via AddReadyzCheck so non-leader pods fail
the /readyz probe and are excluded from Service endpoints.
This keeps cmd/catalogd/main.go health/readiness setup identical to
cmd/operator-controller/main.go (healthz.Ping for both liveness and
readiness); the catalog-server readiness check is an implementation detail
of serverutil.AddCatalogServerToManager.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings April 27, 2026 18:12
@tmshort
tmshortforce-pushed the fix-catalogd-ha-readiness branch from 99db2dc to 3bebd05CompareApril 27, 2026 18:12
@openshift-ciopenshift-ciBot removed the lgtm Indicates that a PR is ready to be merged. label Apr 27, 2026
@openshift-ci

Copy link
Copy Markdown

New changes are detected. LGTM label has been removed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to +68
// catalogServerRunnable is a leader-only Runnable that binds the catalog HTTP port
// lazily inside Start(), so non-leader pods never hold the listen socket.

CopilotAIApr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The struct comment says catalogServerRunnable is "leader-only" and that non-leader pods never hold the listen socket, but the implementation sets NeedLeaderElection() to false and listens/serves on every replica. Please update the comments to reflect the actual behavior (or adjust the behavior to match the comment) to avoid misleading future maintainers.

Suggested change
// catalogServerRunnable is a leader-only Runnable that binds the catalog HTTP port
// lazily inside Start(), so non-leader pods never hold the listen socket.
// catalogServerRunnable is a Runnable that starts on every pod because
// NeedLeaderElection returns false. It binds the catalog HTTP port inside
// Start(), so every replica holds the listen socket and may serve requests
// before it becomes leader.

Copilot uses AI. Check for mistakes.
}()

if err := r.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err

CopilotAIApr 27, 2026

Copy link

Choose a reason for hiding this comment

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

If http.Server.Serve fails, the returned error isn't wrapped, which makes it harder to diagnose which server failed in aggregated manager errors/logs. Consider wrapping the error with context (e.g., include the catalog listen address) before returning.

Suggested change
returnerr
returnfmt.Errorf("catalog server serve failed on %q: %w", r.cfg.CatalogAddr, err)

Copilot uses AI. Check for mistakes.
The experimental e2e suite uses a 2-node kind cluster, making it a natural
fit to validate HA behaviour. Set replicas=2 for both components in
helm/experimental.yaml so the experimental and experimental-e2e manifests
exercise the multi-replica path end-to-end.
This is safe for operator-controller (no leader-only HTTP servers) and for
catalogd now that the catalog server starts on all pods via
NeedLeaderElection=false, preventing the rolling-update deadlock that would
arise if the server were leader-only.
Also adds a @CatalogdHA experimental e2e scenario that force-deletes the
catalogd leader pod and verifies that a new leader is elected and the catalog
resumes serving. The scenario is gated on a 2-node cluster (detected in
BeforeSuite and reflected in the featureGates map), so it is automatically
skipped in the standard 1-node e2e suite. The experimental e2e timeout is
bumped from 20m to 25m to accommodate leader re-election time (~163s worst
case).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Todd Short <tshort@redhat.com>
@tmshort
tmshortforce-pushed the fix-catalogd-ha-readiness branch from 3bebd05 to c74adffCompareApril 27, 2026 18:43
@openshift-merge-bot
openshift-merge-botBot merged commit e12d90b into operator-framework:mainApr 27, 2026
26 checks passed
tmshort added a commit to tmshort/operator-controller that referenced this pull request Apr 28, 2026
Two fixes on top of the catalogd HA work (operator-framework#2674):
1. serverutil: wrap http.Server.Serve error with the catalog listen address
so the failure message is self-diagnosing in aggregated manager logs.
2. e2e: fix CatalogdHA gate to override node-count check with actual
catalogd replica count. The node-count heuristic fires on any multi-node
cluster (e.g. OpenShift standard e2e on AWS), causing the @CatalogdHA
scenario to run even when catalogd has only 1 replica, which makes the
step "catalogd is ready to reconcile resources" fail with exit status 1.
After olmNamespace is populated, query the catalogd deployment's
spec.replicas and set the gate unconditionally to (replicas >= 2),
overriding the earlier node-count result. For upgrade scenarios the
detectOLMDeployment early-return keeps the node-count value intact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Todd Short <tshort@redhat.com>
tmshort added a commit to tmshort/operator-controller that referenced this pull request Apr 28, 2026
Two fixes on top of the catalogd HA work (operator-framework#2674):
1. serverutil: wrap http.Server.Serve error with the catalog listen address
so the failure message is self-diagnosing in aggregated manager logs.
2. e2e: fix CatalogdHA gate to override node-count check with actual
catalogd replica count. The node-count heuristic fires on any multi-node
cluster (e.g. OpenShift standard e2e on AWS), causing the @CatalogdHA
scenario to run even when catalogd has only 1 replica, which makes the
step "catalogd is ready to reconcile resources" fail with exit status 1.
After olmNamespace is populated, query the catalogd deployment's
spec.replicas and set the gate unconditionally to (replicas >= 2),
overriding the earlier node-count result. For upgrade scenarios the
detectOLMDeployment early-return keeps the node-count value intact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Todd Short <tshort@redhat.com>
openshift-merge-botBot pushed a commit that referenced this pull request Apr 29, 2026
Two fixes on top of the catalogd HA work (#2674):
1. serverutil: wrap http.Server.Serve error with the catalog listen address
so the failure message is self-diagnosing in aggregated manager logs.
2. e2e: fix CatalogdHA gate to override node-count check with actual
catalogd replica count. The node-count heuristic fires on any multi-node
cluster (e.g. OpenShift standard e2e on AWS), causing the @CatalogdHA
scenario to run even when catalogd has only 1 replica, which makes the
step "catalogd is ready to reconcile resources" fail with exit status 1.
After olmNamespace is populated, query the catalogd deployment's
spec.replicas and set the gate unconditionally to (replicas >= 2),
overriding the earlier node-count result. For upgrade scenarios the
detectOLMDeployment early-return keeps the node-count value intact.
Signed-off-by: Todd Short <tshort@redhat.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@tmshort
tmshort deleted the fix-catalogd-ha-readiness branch May 1, 2026 13:22
tmshort added a commit to tmshort/operator-framework-operator-controller that referenced this pull request Jul 23, 2026
Backport of upstream #2674. In HA topology (replicas=2), the old catalogd
serverutil used OnlyServeWhenLeader=true: the non-leader pod bound the TCP
port via net.Listen so the OS accepted connections, but no HTTP server was
running. Operator-controller hit the non-leader ~50% of the time and the
connection hung silently. With the HTTP client timeout raised to 5m
(OCPBUGS-92037), this hang lasted the full 5 minutes instead of 10s,
causing catalog caches to never be populated within test windows.
Fix:
- catalogd serverutil: switch to NeedLeaderElection=false so all pods serve
HTTP; non-leaders return 404 (empty local cache) instead of hanging.
Register a readiness check so the pod is excluded from endpoints until the
server is ready.
- client.go: do not cache non-200 responses; return the error directly so
the next reconcile retries a fresh HTTP request and eventually hits the leader.
Upstream: operator-framework/operator-controller#2674
Signed-off-by: Todd Short <tshort@redhat.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approvedIndicates a PR has been approved by an approver from all required OWNERS files.do-not-merge/holdIndicates that a PR should not merge because someone has issued a /hold command.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tmshort@dtfranz@camilamacedo86@rashmigottipati