Skip to content

pkg/utils/etcdutil: reload watcher after compaction#11033

Open
rleungx wants to merge 5 commits into
tikv:masterfrom
rleungx:fix-loop-watcher-compaction-reload
Open

pkg/utils/etcdutil: reload watcher after compaction#11033
rleungx wants to merge 5 commits into
tikv:masterfrom
rleungx:fix-loop-watcher-compaction-reload

Conversation

@rleungx

@rleungx rleungx commented Jul 21, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: Close #11032

When an etcd watch revision has been compacted, LoopWatcher resumes from the
compact revision without reloading current state. Changes in the compacted
interval are not replayed, so consumer caches can remain stale.

Paginated loads can also observe different etcd revisions, and an earlier
callback error can be overwritten by a later successful callback.

What is changed and how does it work?

  • Reload current state before restarting a compacted watch.
  • Read every page of one load from the same etcd revision.
  • Preserve the first load callback error and only advance the watch revision
    after a successful load.
  • Add regression coverage for missed compacted changes, paginated snapshot
    consistency, and callback error propagation.
Reload LoopWatcher state from a consistent etcd snapshot after watch
compaction. Preserve the first callback error and only advance the watch
revision after a successful load.

Check List

Tests

  • Unit test

Release note

Fix an issue where LoopWatcher consumers could miss etcd changes after watch compaction.

Summary by CodeRabbit

  • Bug Fixes
    • Improved etcd-backed watcher consistency by enabling deleted-key reconciliation across TSO, metadata, scheduling (store/config/rules/labels/affinity/group), and service primary watchers.
    • Strengthened compaction handling by reloading from the correct revision, adding exponential backoff with a retry-interval cap, and reconciling snapshot-loaded keys after compaction.
    • Reduced rule watcher work by skipping patch commits when a batch has no relevant rule/group changes.
  • Tests
    • Expanded loop-watcher and rule-watcher test coverage for deleted-key reconciliation, compaction reload backoff, lifecycle callback error behavior, and single-revision loading.

Reload from a consistent etcd snapshot when the requested watch revision has been compacted, so changes in the compacted interval are not skipped.

Keep the first load callback error and only advance the watch revision after a successful reload.

Signed-off-by: Ryan Leung <rleungx@gmail.com>
@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/needs-triage-completed dco-signoff: yes Indicates the PR's author has signed the dco. labels Jul 21, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 21, 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 jmpotato for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

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

@ti-chi-bot ti-chi-bot Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d5e5f8e-1bcf-442a-b391-b3d993aa5766

📥 Commits

Reviewing files that changed from the base of the PR and between 492c88c and 03ed7ea.

📒 Files selected for processing (4)
  • pkg/mcs/scheduling/server/rule/watcher.go
  • pkg/mcs/scheduling/server/rule/watcher_test.go
  • pkg/utils/etcdutil/etcdutil.go
  • pkg/utils/etcdutil/etcdutil_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/scheduling/server/rule/watcher.go

📝 Walkthrough

Walkthrough

LoopWatcher now reloads consistent etcd snapshots after compaction, retries with bounded backoff, reconciles deleted keys, and preserves callback errors. TSO, scheduling, routing, and service-primary watchers enable deleted-key reconciliation, with expanded tests and rule-watcher checker injection.

Changes

LoopWatcher consistency

Layer / File(s) Summary
Consistent reload and deletion reconciliation
pkg/utils/etcdutil/etcdutil.go
LoopWatcher loads use a consistent revision, recover from compaction through bounded backoff, track loaded keys for deletion reconciliation, defer state updates until callbacks succeed, and expose SetReconcileDeletedKeys().
Watcher load and compaction tests
pkg/utils/etcdutil/etcdutil_test.go
Tests cover callback errors, single-revision loading, compaction reloads, deleted-key reconciliation, retry behavior, and exponential backoff.
Deleted-key reconciliation wiring
pkg/keyspace/..., pkg/mcs/..., pkg/tso/..., server/server.go
TSO, scheduling, routing, and service-primary watchers enable deleted-key reconciliation before starting their watch loops.
Rule watcher event filtering and test wiring
pkg/mcs/scheduling/server/rule/*
Rule changes now control patch commits and suspect key-range registration; rule and label watchers reconcile deleted keys, while tests inject checker controllers and reuse embedded-etcd cleanup.

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

Sequence Diagram(s)

sequenceDiagram
  participant LoopWatcher
  participant etcd
  participant WatchCallbacks
  LoopWatcher->>etcd: Watch from required revision
  etcd-->>LoopWatcher: Compacted revision response
  LoopWatcher->>etcd: Reload snapshot at consistent revision
  etcd-->>LoopWatcher: Snapshot keys and loaded revision
  LoopWatcher->>WatchCallbacks: Apply snapshot callbacks
  LoopWatcher->>etcd: Resume watch after loaded revision
Loading

Possibly related PRs

  • tikv/pd#10938: Overlaps with the embedded-etcd and prepare test setup in pkg/mcs/scheduling/server/rule/watcher_test.go.

Suggested labels: needs-ok-to-test

Suggested reviewers: jmpotato, bufferflies, myonkeminta

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes many other watchers and server code to enable deleted-key reconciliation, which is outside #11032's stated scope. Split the deleted-key reconciliation watcher changes into a separate PR or explain why they are required for this issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change to LoopWatcher compaction handling.
Description check ✅ Passed The PR description follows the template and includes the issue number, problem, changes, tests, and release note.
Linked Issues check ✅ Passed The changes address #11032 by reloading from a consistent snapshot after compaction and preserving callback error and revision semantics.
✨ 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.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.47368% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.25%. Comparing base (39b6220) to head (492c88c).

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11033      +/-   ##
==========================================
- Coverage   79.25%   79.25%   -0.01%     
==========================================
  Files         541      541              
  Lines       76037    76134      +97     
==========================================
+ Hits        60262    60337      +75     
- Misses      11534    11549      +15     
- Partials     4241     4248       +7     
Flag Coverage Δ
unittests 79.25% <89.47%> (-0.01%) ⬇️

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

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

@rleungx

rleungx commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

/retest

Signed-off-by: Ryan Leung <rleungx@gmail.com>
@rleungx
rleungx requested review from JmPotato and lhy1024 July 21, 2026 09:05
@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 21, 2026
rleungx added 2 commits July 21, 2026 17:26
Signed-off-by: Ryan Leung <rleungx@gmail.com>
Signed-off-by: Ryan Leung <rleungx@gmail.com>
// SetReconcileDeletedKeys enables deletion reconciliation for full loads.
// It must be called before StartWatchLoop. Since this keeps one entry per
// watched key, callers should only enable it for prefixes with bounded key cardinality.
func (lw *LoopWatcher) SetReconcileDeletedKeys() {

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.

Should we also opt the ResourceManager metadata watcher into this mechanism in this PR? It maintains cached resource-group settings and service limits and has a non-no-op delete callback, but its factory exposes only StartWatchLoop and WaitLoad, so it currently cannot call this setter. I verified the generic behavior with a focused unit test: a reload without this opt-in retains a deleted key, while TestWatcherReconcilesDeletedKeysAfterCompaction passes with it enabled. If this watcher is intentionally out of scope, could we document or narrow the stated coverage?

Comment thread pkg/mcs/scheduling/server/rule/watcher.go
Signed-off-by: Ryan Leung <rleungx@gmail.com>
@ti-chi-bot

ti-chi-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@rleungx: 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
pull-error-log-review 03ed7ea link true /test pull-error-log-review
pull-unit-test-next-gen-3 03ed7ea link true /test pull-unit-test-next-gen-3

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.

}
}
if loadCompleted && err == nil && lw.reconcileDeletedKeys {
lw.loadedKeys = snapshotKeys

@lhy1024 lhy1024 Jul 21, 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.

This all-or-nothing replacement loses partial reconciliation progress. reconcileLoadedKeys can successfully apply deletion A, then fail deletion B, while a successful postEventsFn still commits A. A remains in loadedKeys, so the retry invokes A delete again. That is unsafe for existing non-idempotent callbacks, including the rule watcher after its first commit removes the local RuleStorage entry, and the TSO watcher when the service-registry map entry is gone. A transient B failure can therefore leave compaction reload retrying forever.

I reproduced this on 03ed7ea: a two-key focused test with one successful delete and one one-time delete failure fails the second reload with key was already deleted; the same test passes on 492c88c. Please record successful operations at the same successful post-callback commit boundary, or make the callback contract explicitly idempotent, and add a regression test such as:

func (suite *loopWatcherTestSuite) TestWatcherRetriesPartialReconciliationDelete() {
	re := suite.Require()
	const prefix = "TestWatcherRetriesPartialReconciliationDelete/"
	firstKey, secondKey := prefix+"first", prefix+"second"
	suite.put(re, firstKey, "first")
	suite.put(re, secondKey, "second")

	cache := make(map[string]string)
	transientErr := errors.New("transient delete error")
	alreadyDeletedErr := errors.New("key was already deleted")
	failSecondDelete := true
	watcher := NewLoopWatcher(
		suite.ctx, &suite.wg, suite.client, "test", prefix,
		func([]*clientv3.Event) error { return nil },
		func(kv *mvccpb.KeyValue) error {
			cache[string(kv.Key)] = string(kv.Value)
			return nil
		},
		func(kv *mvccpb.KeyValue) error {
			key := string(kv.Key)
			if key == secondKey && failSecondDelete {
				failSecondDelete = false
				return transientErr
			}
			if _, ok := cache[key]; !ok {
				return alreadyDeletedErr
			}
			delete(cache, key)
			return nil
		},
		func([]*clientv3.Event) error { return nil },
		true,
	)
	watcher.SetReconcileDeletedKeys()

	_, err := watcher.load(suite.ctx)
	re.NoError(err)
	_, err = suite.client.Delete(suite.ctx, firstKey)
	re.NoError(err)
	_, err = suite.client.Delete(suite.ctx, secondKey)
	re.NoError(err)

	_, err = watcher.load(suite.ctx)
	re.ErrorIs(err, transientErr)
	_, err = watcher.load(suite.ctx)
	re.NoError(err)
	re.Empty(cache)
}

etcdClient: client,
ruleStorage: storage,
regionLabeler: labelerManager,
checkerController: checkerController,

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.

Minor: could this helper close rw after the assertion instead of only canceling its context? Watcher.Close() waits for the label watcher and clears the injected checker controller suspect ranges. BenchmarkLoadLargeRules now reuses one controller across iterations, so without that cleanup later samples inherit the previous 16K ranges and can overlap with the previous watcher teardown. A defer rw.Close() after successful initialization would make each iteration independent.

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

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LoopWatcher can miss changes after etcd compaction

2 participants