Skip to content

CNTRLPLANE-3460: Refactor defrag controller to lower impact of defrag#1618

Open
dusk125 wants to merge 7 commits into
openshift:mainfrom
dusk125:defrag-time-budget
Open

CNTRLPLANE-3460: Refactor defrag controller to lower impact of defrag#1618
dusk125 wants to merge 7 commits into
openshift:mainfrom
dusk125:defrag-time-budget

Conversation

@dusk125

@dusk125 dusk125 commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Refactors the defrag controller to defrag one member per sync cycle instead of all members at once
  • Members are sorted by fragmentation descending; the most fragmented member is defragged first
  • The health check at the start of each sync ensures the previously defragged member recovered before the next one is defragged
  • Removes the blocking wait.Poll recovery loop and leader-last ordering — with upstream etcd defrag improvements (openshift/etcd#378) reducing disruption from ~30s to ~100ms, the simpler one-per-sync approach is sufficient

Test plan

  • Existing unit tests updated for multi-sync behavior and pass
  • TestNewDefragControllerMultiSyncs validates degraded/recovery across sync cycles
  • Manual verification in a test cluster with fragmented etcd members

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Defragmentation now targets at most one fragmented member per sync cycle, prioritized by highest backend fragmentation.
    • Member selection skips learner and unstarted members; when no fragmentation is found, degraded-state tracking is cleared and failure counters reset.
    • Defragmentation now fails fast on the first encountered defrag error, updating degraded-state progress immediately.
  • Tests
    • Updated controller tests to support repeated sync loops and adjusted expected success/error behavior.
    • Enhanced the fake client defragmentation behavior used by tests to better reflect status changes.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 13, 2026
@openshift-ci

openshift-ci Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

DefragController's runDefrag method was refactored to process a single fragmented member per sync cycle. It now filters learners and unstarted members upfront, sorts endpoints by fragmentation percentage descending, and attempts defragmentation only on the first fragmented member. Defrag failures now immediately increment failure counters and trigger degraded state transitions. Wait/poll logic and multi-member aggregation were removed. Tests were updated to support multiple sync loops per scenario with adjusted error generation. The fake Etcd client's Defragment method now synchronizes stored status responses.

Changes

DefragController single-member refactor and test updates

Layer / File(s) Summary
Imports, constants, and struct cleanup
pkg/operator/defragcontroller/defragcontroller.go
Added cmp and slices imports for sorting; removed defrag wait/poll timing constants; removed defragWaitDuration field from DefragController struct.
runDefrag rewrite and member type addition
pkg/operator/defragcontroller/defragcontroller.go
Introduced StatusMember struct pairing members with endpoint status. Rewrote runDefrag to filter learner/unstarted members, collect member+status pairs, sort by fragmentation percentage descending, and defragment only the first fragmented endpoint with immediate failure counting and degraded state transitions. Removed leader-last ordering, multi-member aggregation, and cluster stabilization polling.
Test scenario structure and execution updates
pkg/operator/defragcontroller/defragcontroller_test.go
Added syncLoops field to test scenarios to control iteration count. Updated TestNewDefragController to invoke controller.sync in a loop per scenario loop count. Simplified defrag success event validation to count matching messages only, removing leader-last ordering check. Removed unused regexp import.
Multi-sync test scenario adjustments
pkg/operator/defragcontroller/defragcontroller_test.go
Reduced fake defrag error generation counts in TestNewDefragControllerMultiSyncs; increased syncLoops for recovery scenario with corresponding error count adjustments; removed defragWaitDuration override from test controller configuration.
Fake Etcd client status response synchronization
pkg/etcdcli/helpers.go
fakeEtcdClient.Defragment now updates the stored StatusResponse for the target member by copying DbSizeInUse into DbSize for the matching MemberId.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 13 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Structure And Quality ⚠️ Warning TestNewDefragControllerMultiSyncs has 4 assert.Equal() calls without meaningful failure messages, violating assertion quality requirement #4 and inconsistent with TestNewDefragController pattern. Add message parameters to assert.Equal() calls in TestNewDefragControllerMultiSyncs to help diagnose failures (e.g., assert.Equal(t, expected, actual, "failed to match expected sync error loops")).
✅ Passed checks (13 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and specifically describes the main change: refactoring the defrag controller to reduce the impact of defragmentation operations.
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 This PR contains only standard Go testing framework tests (testing.T with t.Run() subtests). The custom check requires Ginkgo test names to be stable and deterministic. Since this codebase uses sta...
Microshift Test Compatibility ✅ Passed No Ginkgo e2e tests were added in this PR. Changes are to controller logic and standard Go unit tests only; the check does not apply.
Single Node Openshift (Sno) Test Compatibility ✅ Passed This PR modifies only standard Go unit tests in pkg/operator/defragcontroller and pkg/etcdcli, not Ginkgo e2e tests. The check for SNO compatibility only applies to new Ginkgo e2e tests; no such te...
Topology-Aware Scheduling Compatibility ✅ Passed PR modifies only controller business logic (defrag strategy) and tests, not deployment manifests or scheduling constraints. Existing topology-aware checks remain in place, disabling defrag on unsup...
Ote Binary Stdout Contract ✅ Passed No stdout-writing patterns detected in modified files. All code changes are in regular functions (not init/main/suite setup), no fmt.Print/Println calls, and klog uses correct stderr default behavior.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed This PR does not add any Ginkgo e2e tests. It modifies standard Go unit tests using testing.T framework, which are outside the scope of this IPv6/disconnected network check.
No-Weak-Crypto ✅ Passed No weak cryptography (MD5, SHA1, DES, RC4, 3DES, Blowfish, ECB), custom crypto implementations, or non-constant-time secret comparisons were introduced in this PR.
Container-Privileges ✅ Passed This PR modifies only Go source files (controller logic and tests), not container/K8s manifests. The privilege check is not applicable since no manifest files with privileged settings are changed.
No-Sensitive-Data-In-Logs ✅ Passed PR contains no sensitive data (passwords, tokens, API keys, PII, session IDs, internal hostnames) in any logging statements. All logged data are operational metrics (member names, IDs, DB sizes, fr...

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci

openshift-ci Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign ingvagabund for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@coderabbitai coderabbitai Bot 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.

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 `@pkg/operator/defragcontroller/defragcontroller_test.go`:
- Around line 464-468: The test uses a tiny non-zero defragBudget which can be
flaky; set the DefragController's defragBudget to 0 (or switch the controller to
use a controllable/test clock) so budget exhaustion is deterministic. Locate
where defragBudget is set in the test setup (the variable named defragBudget
used to construct the controller) and replace 1 * time.Nanosecond with 0 (or
adapt the controller constructor to accept a mock clock and use that in the
test) so calling controller.sync(context.TODO(),
factory.NewSyncContext("defrag-controller", eventRecorder)) always observes an
exhausted budget.
🪄 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: Enterprise

Run ID: e0de8aac-2c9a-42a2-9dee-693e18c04822

📥 Commits

Reviewing files that changed from the base of the PR and between c0614ca and 2c8da6e.

📒 Files selected for processing (2)
  • pkg/operator/defragcontroller/defragcontroller.go
  • pkg/operator/defragcontroller/defragcontroller_test.go

Comment thread pkg/operator/defragcontroller/defragcontroller_test.go Outdated
@dusk125 dusk125 changed the title WIP: Add time budget to defrag controller WIP: CNTRLPLANE-3460: Add time budget to defrag controller May 14, 2026
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label May 14, 2026
@openshift-ci-robot

openshift-ci-robot commented May 14, 2026

Copy link
Copy Markdown

@dusk125: This pull request references CNTRLPLANE-3460 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 sub-task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

  • Adds a fixed 5-minute time budget per defrag controller reconciliation loop
  • Before each member's defrag, the controller checks elapsed time; if the budget is exceeded, remaining members are skipped and deferred to the next sync cycle
  • Budget-skipped members do not count toward the degraded failure threshold — only real defrag errors trigger degradation

Test plan

  • Existing unit tests pass unchanged
  • New TestDefragBudgetExceeded test verifies budget enforcement with a near-zero budget (1ns): no defrags occur, a DefragControllerBudgetExceeded event is recorded, and the controller does not degrade
  • Manual verification in a test cluster with fragmented etcd members

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

  • Added a configurable time budget for defragmentation that stops further member processing when exceeded.

  • Improvements

  • Enhanced tracking to report attempted vs. successful defragmentations.

  • Improved error accounting and more accurate partial-failure reporting when budget limits or mapping errors occur.

  • Tests

  • Added a test validating behavior when the defragmentation budget is immediately exhausted.

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.

@dusk125 dusk125 changed the title WIP: CNTRLPLANE-3460: Add time budget to defrag controller WIP: CNTRLPLANE-3460: Refactor defrag controller to lower impact of defrag May 15, 2026
@dusk125
dusk125 marked this pull request as ready for review May 15, 2026 14:22
@dusk125 dusk125 changed the title WIP: CNTRLPLANE-3460: Refactor defrag controller to lower impact of defrag CNTRLPLANE-3460: Refactor defrag controller to lower impact of defrag May 15, 2026
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 15, 2026
@openshift-ci
openshift-ci Bot requested review from ardaguclu and p0lyn0mial May 15, 2026 14:22
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
@dusk125
dusk125 force-pushed the defrag-time-budget branch from de3f881 to 28f0bd1 Compare June 16, 2026 16:10

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
pkg/operator/defragcontroller/defragcontroller.go (1)

184-190: ⚡ Quick win

Error chain is lost by double-formatting.

The error is first formatted with %v into errMsg, then wrapped with fmt.Errorf("%s", errMsg). This loses the original error chain, preventing callers from using errors.Is/errors.As for inspection.

♻️ Suggested fix to preserve error chain
-		errMsg := fmt.Sprintf("failed defrag on member: %s, memberID: %x: %v", member.Name, member.ID, err)
-		recorder.Eventf("DefragControllerDefragmentFailed", errMsg)
+		recorder.Eventf("DefragControllerDefragmentFailed", "failed defrag on member: %s, memberID: %x: %v", member.Name, member.ID, err)
 		c.numDefragFailures++
 		if c.numDefragFailures >= maxDefragFailuresBeforeDegrade {
 			c.setDegraded(ctx, recorder)
 		}
-		return fmt.Errorf("%s", errMsg)
+		return fmt.Errorf("failed defrag on member: %s, memberID: %x: %w", member.Name, member.ID, err)
🤖 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/operator/defragcontroller/defragcontroller.go` around lines 184 - 190,
The error is being double-formatted which loses the error chain: first with
fmt.Sprintf using %v into errMsg, then wrapped with fmt.Errorf("%s", errMsg).
This prevents callers from using errors.Is or errors.As for inspection. In the
return statement at the end of this block, replace fmt.Errorf("%s", errMsg) with
fmt.Errorf that includes the descriptive message prefix and wraps the original
err variable using the %w verb instead of %v, so the error chain is preserved
while maintaining the same log message for the recorder.Eventf call.
🤖 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.

Nitpick comments:
In `@pkg/operator/defragcontroller/defragcontroller.go`:
- Around line 184-190: The error is being double-formatted which loses the error
chain: first with fmt.Sprintf using %v into errMsg, then wrapped with
fmt.Errorf("%s", errMsg). This prevents callers from using errors.Is or
errors.As for inspection. In the return statement at the end of this block,
replace fmt.Errorf("%s", errMsg) with fmt.Errorf that includes the descriptive
message prefix and wraps the original err variable using the %w verb instead of
%v, so the error chain is preserved while maintaining the same log message for
the recorder.Eventf call.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: f3c9859e-40e4-4363-bee9-a4aff47a4ba3

📥 Commits

Reviewing files that changed from the base of the PR and between de3f881 and 28f0bd1.

📒 Files selected for processing (3)
  • pkg/etcdcli/helpers.go
  • pkg/operator/defragcontroller/defragcontroller.go
  • pkg/operator/defragcontroller/defragcontroller_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/etcdcli/helpers.go

@dusk125

dusk125 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

With the improvements from openshift/etcd#378, the current implementation should be sufficient to lower the impact of defrag. The changes to etcd can take defrag disruption from 30s to 100ms. The current implementation of the changes to the defrag controller run a single defrag, on the most fragmented member, then check the member health on the next sync (once every 11 minutes) and only continue defragging if all members are healthy.

@dusk125
dusk125 force-pushed the defrag-time-budget branch from 0934524 to c92f5ae Compare June 16, 2026 17:38

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/operator/defragcontroller/defragcontroller.go (1)

264-268: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against division by zero when ondisk is zero.

If DbSize is zero (unlikely but possible edge case), this function returns NaN (when inuse is also 0) or -Inf, which can cause unpredictable behavior in the sorting comparator at lines 168-173.

Proposed fix
 func checkFragmentationPercentage(ondisk, inuse int64) float64 {
+	if ondisk <= 0 {
+		return 0
+	}
 	diff := float64(ondisk - inuse)
 	fragmentedPercentage := (diff / float64(ondisk)) * 100
 	return math.Round(fragmentedPercentage*100) / 100
 }
🤖 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/operator/defragcontroller/defragcontroller.go` around lines 264 - 268,
The checkFragmentationPercentage function performs division by ondisk without
checking if it is zero, which results in NaN or Inf and causes unpredictable
behavior in the sorting comparator. Add a guard clause at the start of the
checkFragmentationPercentage function to check if ondisk is less than or equal
to zero, and return 0.0 in that case before performing the division calculation.
🤖 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/operator/defragcontroller/defragcontroller.go`:
- Around line 156-158: The error message in the status nil check at the failing
condition does not identify which member had the validation failure. Modify the
fmt.Errorf call to include the member or endpoint identifier (such as a member
name, IP address, or other relevant context variable that should be available in
the surrounding scope) in addition to a descriptive message, so the error
clearly indicates which specific member's endpoint status validation failed.

---

Outside diff comments:
In `@pkg/operator/defragcontroller/defragcontroller.go`:
- Around line 264-268: The checkFragmentationPercentage function performs
division by ondisk without checking if it is zero, which results in NaN or Inf
and causes unpredictable behavior in the sorting comparator. Add a guard clause
at the start of the checkFragmentationPercentage function to check if ondisk is
less than or equal to zero, and return 0.0 in that case before performing the
division calculation.
🪄 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: Enterprise

Run ID: b38cd00a-ae65-4251-85d7-30bfeb973803

📥 Commits

Reviewing files that changed from the base of the PR and between 28f0bd1 and c92f5ae.

📒 Files selected for processing (1)
  • pkg/operator/defragcontroller/defragcontroller.go

Comment thread pkg/operator/defragcontroller/defragcontroller.go
@dusk125
dusk125 force-pushed the defrag-time-budget branch from c92f5ae to 29119b9 Compare June 16, 2026 17:49
@dusk125

dusk125 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

/retest-required

2 similar comments
@dusk125

dusk125 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

/retest-required

@dusk125

dusk125 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

/retest-required

Changes the defrag controller to defrag a single member per sync cycle
instead of all members in one pass. This reduces the blast radius of
defrag operations and avoids write blocks by preemptively transferring
leadership away from the defrag target.

After a successful leader transfer, the controller requeues with a short
delay to allow etcd to settle before proceeding with defragmentation.
Defrag failures no longer cause immediate requeue; instead the controller
waits for the next periodic sync. The operator client informer is
registered as a bare informer since the controller only uses it for
cache warmup, not watch-triggered syncs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dusk125
dusk125 force-pushed the defrag-time-budget branch from 25d7cc7 to dffee92 Compare July 9, 2026 13:48
Comment on lines +186 to +187
// If the leader is in the first slot, and we have other members to defrag, defrag the others first,
// so we limit the number of times we move the leader during defrag.

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.

in a high churn environment, wouldn't it be possible for leader to keep getting skipped while other members are repeatedly defragged and refragged? Especially since with this change defrag happens more slowly.

Perhaps there should be a threshold at which the leader is selected for defrag even when there are other candidates.

@dusk125 dusk125 Jul 9, 2026

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.

This is a good point, let me have a little think about this... it's tangentially related to us being slower to react to the database getting close to quota. I've been noodling on a "fast mode" or something that detects these scenarios and does a requeue faster so it's not ~30m to defrag all 3 members

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.

I'm open to suggestions as well

@bhperry bhperry Jul 9, 2026

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.

My first thought is setting a "critical threshold" above the standard 45% (say 65% for now) which will force the leader to defrag. But with high churn that could end up always selecting the leader and forcing it to move on every sync if each member is above 65% by the time it runs. No good.

Seems like it would need to record which members have already been defragged recently in order to ensure the leader isn't starved while also avoiding selecting the leader too eagerly.

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.

If we're not overly concerned with recovering the exact state on CEO restart, we could just store that data like the failed defrag count?

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.

I was beginning to think it looked a lot like the original looped version with extra steps, so that's a good call. Certainly much more straightforward this way.

@atiratree atiratree Jul 13, 2026

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.

There was a bug where a defrag would fail and immediately try to defrag the next member.

I am a bit confused here as to where this bug occurs.

keep track of who's been defragged

Don't we always have this information via isEndpointBackendFragmented?

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.

This was in the previous implementation in the first commit in which a single member was defragged each sync. In that case you don't know which members were previously defragged, only which members are currently over the fragmentation threshold. So if a cluster is fragmenting fast enough it would be possible for the same members to keep going over the threshold by the next sync, and the leader would always get skipped.

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.

I wonder how likely that is - maybe we could create an alert for such high fragmentation rate?

I suppose we could also add failsafe and prefer a leader when it goes over a high percentage? e.g. 85%?

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.

I wonder how likely that is - maybe we could create an alert for such high fragmentation rate?

Probably not common, but while running churn tests with kube-burner I have seen fragmentation jump up ~3GB in under a minute.

I suppose we could also add failsafe and prefer a leader when it goes over a high percentage? e.g. 85%?

My worry with a hard threshold to always select the leader is that if all members are highly fragmented it would end up selecting the leader for defrag every time. Or at the very least, for the first two defrags.

Sync 1:

  • etcd1 (leader, 86%) - etcd2 (86%) - etcd3 (86%)
  • defrag etcd1 since it is over critical threshold, leader moves to etcd2

Sync 2:

  • etcd1 (0%) - etcd2 (leader, 86%) - etcd3 (86%)
  • defrag etcd2 since it is over critical threshold, leader moves to etcd1 (in the best case)

...

Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
dusk125 added 2 commits July 13, 2026 11:03
Replace the cached member client with the non-cached etcd client for
the defrag controller's member health and list calls. The defrag
controller syncs infrequently enough that caching provides no benefit,
and the cached client's 1-minute TTL could mask unhealthy members
between rapid defrag operations.

Increase the leader transfer settle time from 5s to 10s (renamed from
leaderTransferSettleTime to defragSettleTime) to allow sufficient time
for the cluster to stabilize after a leadership change.

After a successful defrag, if there are remaining defrag targets,
requeue with the shorter defragSettleTime rather than waiting for the
full ~11 minute compaction-aligned resync period. Non-leader defrags
don't block cluster writes, and the live health check on each sync
ensures the previously defragged member has recovered before proceeding.
Track which members have been successfully defragged during the current
defrag cycle via a defraggedMembers set. When picking the next defrag
target, skip the leader only if there are non-leader targets that
haven't been defragged yet this cycle. Once all non-leaders have been
handled, allow the leader through for defrag (with leader transfer).

The set is cleared when no members need defragmentation, starting a
fresh cycle on the next compaction interval.

This addresses the scenario where in a high-churn environment, all
members stay above the fragmentation threshold across sync cycles.
Without this tracking, len(defragTargets) > 1 would always be true
and the leader would be perpetually skipped in favor of re-fragmented
non-leaders.
@dusk125

dusk125 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Considering a different approach, before merging, I'll squash if the loop is the way we want to go

Comment thread pkg/operator/defragcontroller/defragcontroller.go
Comment on lines +186 to +187
// If the leader is in the first slot, and we have other members to defrag, defrag the others first,
// so we limit the number of times we move the leader during defrag.

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.

I was beginning to think it looked a lot like the original looped version with extra steps, so that's a good call. Certainly much more straightforward this way.

Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
@dusk125
dusk125 force-pushed the defrag-time-budget branch from 806c73f to 855e1e0 Compare July 14, 2026 16:56
@dusk125
dusk125 force-pushed the defrag-time-budget branch from 855e1e0 to f4bf8bb Compare July 14, 2026 17:19
@dusk125

dusk125 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

/retest-required

MoveLeader must be sent to the current leader, but the client pool may
route to any member, causing consistent "etcdserver: not leader" errors
and falling through to blocking leader defrag every time.

Fix by accepting the leader member at the call site and creating a
dedicated client connected directly to the leader endpoint, following
the same pattern used by Defragment().

Also enable the StopGRPCServiceOnDefrag feature gate so etcd reports
NOT_SERVING during defrag, allowing load-balancer-aware clients to
route away from the member being defragged.

Additionally fix the DefragControllerLeaderTransferFailed event which
used Warning instead of Warningf, leaving %s uninterpolated.
@dusk125

dusk125 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

/pipeline

@dusk125

dusk125 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

/pipeline required

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Scheduling required tests:
/test e2e-agnostic-ovn
/test e2e-agnostic-ovn-upgrade
/test e2e-aws-ovn-serial-1of2
/test e2e-aws-ovn-serial-2of2
/test e2e-aws-ovn-single-node
/test e2e-gcp-operator
/test e2e-gcp-operator-disruptive
/test e2e-metal-ipi-ovn-ipv6
/test e2e-operator

Comment thread pkg/operator/defragcontroller/defragcontroller.go
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
Comment on lines +186 to +187
// If the leader is in the first slot, and we have other members to defrag, defrag the others first,
// so we limit the number of times we move the leader during defrag.

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.

I wonder how likely that is - maybe we could create an alert for such high fragmentation rate?

I suppose we could also add failsafe and prefer a leader when it goes over a high percentage? e.g. 85%?

Comment thread pkg/operator/defragcontroller/defragcontroller.go Outdated
@dusk125
dusk125 force-pushed the defrag-time-budget branch from a8869f0 to 40ead87 Compare July 22, 2026 15:05
@dusk125

dusk125 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

/pipeline required

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Scheduling required tests:
/test e2e-agnostic-ovn
/test e2e-agnostic-ovn-upgrade
/test e2e-aws-ovn-serial-1of2
/test e2e-aws-ovn-serial-2of2
/test e2e-aws-ovn-single-node
/test e2e-gcp-operator
/test e2e-gcp-operator-disruptive
/test e2e-metal-ipi-ovn-ipv6
/test e2e-operator

for _, member := range members {
// filter out learner members since they don't support the defragment API call
// and filter out unstarted members
if member.IsLearner || len(member.ClientURLs) == 0 {

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.

Is there any possibility of a member having either of these conditions (e.g. becoming a learner, not sure how that works) after it was already added to defragTargets in a previous sync?

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.

Maybe....? it would be unlikely unless the cluster is scaling or something like that where we have quorum but additional members that haven't been started, or are new and haven't been promoted yet.

How come?

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.

Here they are intentionally not being added to the list. But if they are already on the list and then later become a learner then they may attempt to defrag and then fail, possibly becoming a blocker that is stuck in the list.

@bhperry bhperry Jul 22, 2026

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.

May not be a possible scenario. More generally, I am wondering what happens if there is a member in the defragTargets list that repeatedly fails to defragment, or if a member was removed from the cluster but is still in the list. Seems like that could prevent other members from even being considered for defragmentation.

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.

Ahhh good catch, I don't think it's likely, but it's easy enough to protect against.

Comment thread pkg/operator/defragcontroller/defragcontroller.go
Enable the NonBlockingDefrag etcd feature gate so that defragmentation
does not hold exclusive locks for the entire copy operation, avoiding
write stalls during defrag.

Also change the defrag controller to only defrag one member per sync
cycle rather than immediately requeueing for the next member. This
lets the normal 11-minute resync handle subsequent members, giving the
cluster a full compaction interval to recover between defrags.
@dusk125
dusk125 force-pushed the defrag-time-budget branch from 40ead87 to 680d8a1 Compare July 22, 2026 18:26
recorder.Eventf("DefragControllerDefragmentAttempt", "Attempting defrag on member: %s, memberID: %x, dbSize: %d, dbInUse: %d, leader ID: %d", defragTargetMember.Name, defragTargetMember.ID, defragTargetStatus.DbSize, defragTargetStatus.DbSizeInUse, defragTargetStatus.Leader)
if _, err := c.defragClient.Defragment(ctx, defragTargetMember); err != nil {
errMsg := fmt.Sprintf("failed defrag on member: %s, memberID: %x: %v", defragTargetMember.Name, defragTargetMember.ID, err)
recorder.Eventf("DefragControllerDefragmentFailed", errMsg)

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.

I still see the event suggestion unresolved

@dusk125
dusk125 force-pushed the defrag-time-budget branch from 278e4ea to 3cc0bca Compare July 23, 2026 14:28
@bhperry

bhperry commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Some changes we were discussing in slack:

  • If there are existing defragTargets in the list, they should be checked against the defrag threshold again before running defrag. Fragmentation can naturally decrease over time in an active cluster, so if a member ID has been in the list for a couple cycles it is possible that it is well below the fragmentation threshold and running defrag could result in a large spike in memory (worst case the full size of the database). Currently the status is only used to resort the list, not to check threshold.
  • Ensure that all IDs in the list belong to current etcd members. If a member is removed from the cluster while it is already in the list that could lead to unexpected behavior such as a nil pointer deref or silently failing and never rebuilding the defragTargets list.

@dusk125
dusk125 force-pushed the defrag-time-budget branch from 3cc0bca to ccc4a52 Compare July 24, 2026 15:14
@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@dusk125: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-aws-ovn-serial-2of2 40ead87 link true /test e2e-aws-ovn-serial-2of2
ci/prow/e2e-gcp-operator 40ead87 link true /test e2e-gcp-operator
ci/prow/e2e-aws-ovn-serial-1of2 40ead87 link true /test e2e-aws-ovn-serial-1of2
ci/prow/e2e-metal-ipi-ovn-ipv6 40ead87 link true /test e2e-metal-ipi-ovn-ipv6
ci/prow/e2e-operator 40ead87 link true /test e2e-operator
ci/prow/e2e-gcp-operator-disruptive 40ead87 link true /test e2e-gcp-operator-disruptive
ci/prow/e2e-agnostic-ovn 40ead87 link true /test e2e-agnostic-ovn
ci/prow/e2e-aws-ovn-single-node 40ead87 link true /test e2e-aws-ovn-single-node
ci/prow/configmap-scale ccc4a52 link false /test configmap-scale

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants