Skip to content

resource_group: implement async loading for resource groups#10873

Open
bufferflies wants to merge 14 commits into
tikv:masterfrom
bufferflies:pr-cp-10872-async-load-resource-groups
Open

resource_group: implement async loading for resource groups#10873
bufferflies wants to merge 14 commits into
tikv:masterfrom
bufferflies:pr-cp-10872-async-load-resource-groups

Conversation

@bufferflies

@bufferflies bufferflies commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #10872, ref #10516

This CP ports the upstream Resource Manager change from source commit da1b8ba1e3873401aef0fcbd99c0898a654952e5 to improve Resource Manager startup latency when many resource groups exist.

What is changed and how does it work?

resource_group: implement async loading for resource groups
  • Load resource group metadata asynchronously during Resource Manager initialization.
  • Add lazy single-group loading so point operations can proceed before the full async load completes.
  • Return ErrResourceGroupsLoading for list requests until async loading completes.
  • Track synchronously loaded/changed groups so async merge does not resurrect deleted groups or overwrite live changes.
  • Add single resource group setting/state load APIs and resource group loading metrics.
  • Adapt the implementation to current master keyspace-aware resource group managers and metadata watcher mode.

Source commit: da1b8ba1e3873401aef0fcbd99c0898a654952e5
Original author: disksing i@disksing.com

Check List

Tests

  • Unit test
  • Integration test

Validation:

  • GOFLAGS=-buildvcs=false go test ./pkg/mcs/resourcemanager/server -run 'TestAsyncLoadResourceGroups|TestManagerMetadataWatcherLifecycle|TestInitManager|TestLoadKeyspaceResourceGroupsRejectsMismatchedPayloadName' -count=1 -timeout=90s
  • GOFLAGS=-buildvcs=false go test ./pkg/storage ./pkg/mcs/resourcemanager/server -run '^$' -count=1 -timeout=5m
  • GOFLAGS=-buildvcs=false go test ./tests/integrations/mcs/resourcemanager -run '^$' -count=1 -timeout=10m could not run from the root module because integration tests are a separate Go module.
  • From tests/integrations: GOFLAGS=-buildvcs=false go test ./mcs/resourcemanager -run '^$' -count=1 -timeout=10m was blocked by pre-existing dashboard embedded asset generation errors (undefined: assets, undefined: vfsgen۰FS).

Code changes

  • Has persistent data change

Side effects

  • Increased code complexity

Related changes

  • Need to cherry-pick to the release branch

Release note

None.

Summary by CodeRabbit

  • New Features
    • Resource groups can now be accessed individually while the full resource-group list is still loading.
    • Added loading-state awareness: listing resource groups returns a retryable error while async load is in progress.
    • Improved async loading across startup and updates, including recovery behavior and reserved placeholder handling.
    • Added monitoring metrics for sync vs. async resource-group loading.
  • Bug Fixes
    • Fixed cases where deleted or placeholder resource groups could reappear after asynchronous loading.
    • Improved token acquisition to ensure resource-group lazy-loading happens before applying service limits.

…e startup performance (tikv#411)

* feat: implement async loading for resource groups

- Add async loading mechanism to reduce startup time
- Use atomic operations for loading state management
- Implement lazy loading for individual resource groups
- Add retry mechanism with infinite retries for reliability
- Return error for list requests during loading
- Extend storage interface for single group loading
- Optimize loading logic to avoid partial loading issues

This change significantly improves startup performance by loading
resource groups asynchronously while maintaining data integrity.

Signed-off-by: disksing <i@disksing.com>

* tiny fix

Signed-off-by: disksing <i@disksing.com>

* test: add comprehensive async loading test with simplified blocking control

- Simplify control mechanism from 4 to 2 control points:
  * blockBeforeLoad: blocks before starting load operation
  * blockAfterLoad: blocks after loading is completed
- Add more test data (test-group-3, test-group-4) for better coverage
- Test operations during async loading (read, update, delete, list)
- Test operations after async loading completes
- Verify syncLoadedGroups mechanism prevents group resurrection
- Ensure proper error handling during loading state

This test validates the complete async loading workflow with
simplified control and comprehensive scenario coverage.

Signed-off-by: disksing <i@disksing.com>

* fix: resolve testifylint issues in test files

- Remove unnecessary fmt.Sprintf calls in assert messages
- Use require instead of assert for error assertions
- Remove unused fmt imports

This fixes all testifylint warnings in the test files.

Signed-off-by: disksing <i@disksing.com>

* fix: replace assert.NoError with require.NoError in manager_async_test.go

- Fix testifylint require-error violations on lines 342, 347, and 353
- Use require.NoError for error assertions to ensure test stops on failure

Signed-off-by: disksing <i@disksing.com>

* feat: add metrics for resource group loading operations

- Add asyncLoadGroupDuration histogram to track async loading performance
- Add syncLoadGroupCounter to count synchronous loading operations
- Include duration in async loading completion logs for better observability

This helps monitor the performance of resource group loading and understand
the loading patterns in the system.

Signed-off-by: disksing <i@disksing.com>

* update error code

Signed-off-by: disksing <i@disksing.com>

* minor fix

Signed-off-by: disksing <i@disksing.com>

* fix default group

Signed-off-by: disksing <i@disksing.com>

* extract addDefaultGroup

Signed-off-by: disksing <i@disksing.com>

* minor fix

Signed-off-by: disksing <i@disksing.com>

* fix static check

Signed-off-by: disksing <i@disksing.com>

* fix lint

Signed-off-by: disksing <i@disksing.com>

* fix manager reload

Signed-off-by: disksing <i@disksing.com>

* fix update default group

Signed-off-by: disksing <i@disksing.com>

* fix test

Signed-off-by: disksing <i@disksing.com>

* fix when load failed

Signed-off-by: disksing <i@disksing.com>

---------

Signed-off-by: disksing <i@disksing.com>
(cherry picked from commit da1b8ba)
@ti-chi-bot ti-chi-bot Bot added release-note-none Denotes a PR that doesn't merit a release note. do-not-merge/needs-triage-completed dco-signoff: yes Indicates the PR's author has signed the dco. labels Jun 10, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jun 10, 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 siddontang 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/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 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: 1ae21e5d-9d83-44f1-a60d-8deed2ae6c1a

📥 Commits

Reviewing files that changed from the base of the PR and between eb3e774 and 3e6fb18.

📒 Files selected for processing (2)
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/resourcemanager/server/manager.go

📝 Walkthrough

Walkthrough

Resource-group initialization now supports asynchronous bulk loading with single-group lazy reads, loading-state errors, reserved placeholder tracking, updated RPC ordering, Prometheus metrics, and tests covering loading, deletion, legacy keyspaces, watcher timing, and restarts.

Changes

Async resource-group loading

Layer / File(s) Summary
Loading contract and storage reads
errors.toml, pkg/errs/errno.go, pkg/storage/endpoint/resource_group.go
Adds ErrResourceGroupsLoading and storage methods for individual resource-group settings and states.
Manager bootstrap and loading state
pkg/mcs/resourcemanager/server/manager.go, pkg/mcs/resourcemanager/server/metrics.go
Adds atomic loading states, asynchronous bulk loading, synchronous bootstrap loading, reserved-cache initialization, and loading metrics.
Reserved placeholder coordination
pkg/mcs/resourcemanager/server/keyspace_manager.go
Tracks synthetic default placeholders, clears reservations after confirmed updates, and skips persistence of reserved running state.
Lazy access and API coordination
pkg/mcs/resourcemanager/server/manager.go, pkg/mcs/resourcemanager/server/grpc_service.go
Loads groups before API access, tracks synchronously loaded groups, rejects list requests during loading, and orders token-bucket access around lazy loading.
Async loading behavior tests
pkg/mcs/resourcemanager/server/manager_async_test.go
Tests lazy reads, deletion during loading, legacy keyspace behavior, and recovery from state-load failure.
Asynchronous test synchronization
pkg/mcs/resourcemanager/server/manager_test.go, tests/integrations/mcs/resourcemanager/resource_manager_test.go
Updates tests to wait for asynchronous initialization and normalize runtime fields during restart comparisons.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ResourceManager
  participant ResourceGroupStorage
  participant AsyncLoader
  Client->>ResourceManager: ListResourceGroups()
  ResourceManager-->>Client: ErrResourceGroupsLoading
  Client->>ResourceManager: GetResourceGroup(keyspace,name)
  ResourceManager->>ResourceGroupStorage: LoadResourceGroupSetting/State()
  ResourceGroupStorage-->>ResourceManager: persisted group data
  ResourceManager-->>Client: resource group
  AsyncLoader->>ResourceGroupStorage: Load all resource groups
  ResourceGroupStorage-->>AsyncLoader: persisted settings and states
  AsyncLoader->>ResourceManager: merge unconfirmed groups
  ResourceManager-->>Client: completed resource-group list
Loading

Possibly related PRs

  • tikv/pd#10227: Both change resource-group running-state persistence behavior.
  • tikv/pd#10259: Both extend metadata-watcher and placeholder handling.
  • tikv/pd#10935: Both stabilize asynchronous resource-group integration assertions.

Suggested labels: type/development

Suggested reviewers: disksing, lhy1024, rleungx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: async loading for resource groups.
Description check ✅ Passed The description includes the required problem, change summary, tests, and release note sections and is sufficiently complete.
Linked Issues check ✅ Passed The PR implements the linked issue's objective of asynchronous resource-group loading and related lazy-load behavior.
Out of Scope Changes check ✅ Passed The changes stay focused on async resource-group loading, supporting storage APIs, metrics, and tests with no clear unrelated scope.
✨ 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.

@bufferflies

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

🧹 Nitpick comments (2)
pkg/mcs/resourcemanager/server/metrics.go (2)

232-238: ⚡ Quick win

Improve help text clarity for histogram metric.

The help text "The duration of the async load group." is grammatically awkward and could be clearer.

📝 Proposed help text improvement
 	asyncLoadGroupDuration = prometheus.NewHistogram(
 		prometheus.HistogramOpts{
 			Namespace: namespace,
 			Subsystem: serverSubsystem,
 			Name:      "async_load_group_duration_seconds",
-			Help:      "The duration of the async load group.",
+			Help:      "Duration of asynchronous resource group loading in seconds.",
 		})
🤖 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/mcs/resourcemanager/server/metrics.go` around lines 232 - 238, The
histogram metric asyncLoadGroupDuration created via prometheus.NewHistogram
(prometheus.HistogramOpts with Name "async_load_group_duration_seconds") has an
awkward Help string; update the Help field to a clearer, grammatically correct
description such as "Duration in seconds of async load group operations." so the
metric help explicitly states units and purpose.

224-230: ⚡ Quick win

Follow Prometheus naming conventions for counter metrics.

The metric name sync_load_group_counter violates Prometheus naming conventions:

  • Counter metric names should end with _total (not _counter), per Prometheus best practices.
  • The help text "The number of the sync load group." is grammatically awkward.
📊 Proposed fix for metric naming and help text
 	syncLoadGroupCounter = prometheus.NewCounter(
 		prometheus.CounterOpts{
 			Namespace: namespace,
 			Subsystem: serverSubsystem,
-			Name:      "sync_load_group_counter",
-			Help:      "The number of the sync load group.",
+			Name:      "sync_loaded_groups_total",
+			Help:      "Total number of on-demand resource group loads.",
 		})

Note: This change will require updating the call site in loadResourceGroupIfNeeded (manager.go) if the variable name is also changed.

🤖 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/mcs/resourcemanager/server/metrics.go` around lines 224 - 230, Rename the
Prometheus counter metric and its help text: change the metric Name from
"sync_load_group_counter" to "sync_load_group_total" and update Help to a clear
phrase like "Total number of sync load group operations." Update the variable
syncLoadGroupCounter (and any references to it) so the code and call sites
remain consistent—specifically adjust usages in loadResourceGroupIfNeeded
(manager.go) to reference the renamed metric/variable. Ensure only the metric
Name and Help text semantics change and that all references compile after the
renaming.
🤖 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/mcs/resourcemanager/server/metrics.go`:
- Around line 232-238: The histogram metric asyncLoadGroupDuration created via
prometheus.NewHistogram (prometheus.HistogramOpts with Name
"async_load_group_duration_seconds") has an awkward Help string; update the Help
field to a clearer, grammatically correct description such as "Duration in
seconds of async load group operations." so the metric help explicitly states
units and purpose.
- Around line 224-230: Rename the Prometheus counter metric and its help text:
change the metric Name from "sync_load_group_counter" to "sync_load_group_total"
and update Help to a clear phrase like "Total number of sync load group
operations." Update the variable syncLoadGroupCounter (and any references to it)
so the code and call sites remain consistent—specifically adjust usages in
loadResourceGroupIfNeeded (manager.go) to reference the renamed metric/variable.
Ensure only the metric Name and Help text semantics change and that all
references compile after the renaming.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6d75193-729b-4e08-852a-d96780552f15

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8a5f6 and 23a9e24.

📒 Files selected for processing (8)
  • errors.toml
  • pkg/errs/errno.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
  • pkg/mcs/resourcemanager/server/manager_test.go
  • pkg/mcs/resourcemanager/server/metrics.go
  • pkg/storage/endpoint/resource_group.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go
👮 Files not reviewed due to content moderation or server errors (7)
  • errors.toml
  • pkg/errs/errno.go
  • pkg/storage/endpoint/resource_group.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
  • pkg/mcs/resourcemanager/server/manager_test.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go

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

🧹 Nitpick comments (1)
errors.toml (1)

929-932: 💤 Low value

Error definition placement is inconsistent.

The new ErrResourceGroupsLoading error is placed between scatter and schedule error sections, while other resourcemanager errors are grouped together earlier (lines 899-922). This may indicate the file wasn't regenerated with make generate-errdoc after adding the error code.

As per coding guidelines: "Update errors.toml via make generate-errdoc when adding error codes".

🤖 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 `@errors.toml` around lines 929 - 932, The new error definition
"PD:resourcemanager:ErrResourceGroupsLoading" is placed out of the existing
resourcemanager block; regenerate and reinsert it properly by running the
repository's error doc generator and updating errors.toml via "make
generate-errdoc" so the ErrResourceGroupsLoading entry is grouped with the other
resourcemanager errors (the same section that contains codes from lines
~899-922) and remove the stray entry between scatter and schedule; ensure the
generated output contains the exact error key ErrResourceGroupsLoading and the
original message text.

Source: Coding guidelines

🤖 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/errs/errno.go`:
- Line 544: The ErrResourceGroupsLoading error declaration has misaligned
spacing compared to surrounding error constants; run the project's formatter
(e.g., make fmt or gofmt) and reformat the declaration for
ErrResourceGroupsLoading so its whitespace/indentation matches the other error
declarations (ensure the line defining ErrResourceGroupsLoading uses the same
leading spaces and alignment as the surrounding Err... = errors.Normalize(...)
entries).

In `@pkg/mcs/resourcemanager/server/manager.go`:
- Around line 557-562: When LoadResourceGroupState(keyspaceID, name) returns an
error its value is currently ignored; update the block around
LoadResourceGroupState and setRawStatesIntoResourceGroup so that if err != nil
you log the error with context (keyspaceID and name) using the manager's logger
(e.g., m.logger or existing log facility) before continuing, but still only call
krgm.setRawStatesIntoResourceGroup when err == nil and state != ""; ensure the
log message clearly identifies the failure of m.storage.LoadResourceGroupState
for that resource group.

In `@pkg/mcs/resourcemanager/server/metrics.go`:
- Around line 232-238: The metric asyncLoadGroupDuration is created with
prometheus.NewHistogram and its Help string is vague; update the HistogramOpts
Help to a clear, grammatically complete description such as "Duration of
asynchronous resource group load operations in seconds" or "Duration in seconds
of background async resource group loading" to indicate the measured operation
and units (reference asyncLoadGroupDuration, prometheus.NewHistogram,
HistogramOpts, namespace, serverSubsystem).
- Around line 224-230: The metric defined as syncLoadGroupCounter uses an
unclear Help string and a redundant name; update the prometheus.CounterOpts for
the prometheus.NewCounter call that constructs syncLoadGroupCounter to use a
clearer metric name (e.g., change Name from "sync_load_group_counter" to
"sync_load_groups_total") and improve Help to an explicit sentence such as
"Total number of resource groups loaded synchronously." Ensure you update any
places that reference syncLoadGroupCounter (or the old metric name) so
registration and use remain consistent.

In `@tests/integrations/mcs/resourcemanager/resource_manager_test.go`:
- Around line 275-280: The TestWatchResourceGroup test does a direct
controller.GetResourceGroup(...) followed by re.NotNil(meta) which is flaky due
to async watcher propagation; change that assertion to use testutil.Eventually
like earlier in the test: repeatedly call
controller.GetResourceGroup(group.Name) inside the Eventually predicate and
assert the returned meta is non-nil (and any other expectations) once the
predicate succeeds, mirroring the existing
waitAsyncLoadResourceGroups/testutil.Eventually usage to make the check
resilient to propagation delays.

---

Nitpick comments:
In `@errors.toml`:
- Around line 929-932: The new error definition
"PD:resourcemanager:ErrResourceGroupsLoading" is placed out of the existing
resourcemanager block; regenerate and reinsert it properly by running the
repository's error doc generator and updating errors.toml via "make
generate-errdoc" so the ErrResourceGroupsLoading entry is grouped with the other
resourcemanager errors (the same section that contains codes from lines
~899-922) and remove the stray entry between scatter and schedule; ensure the
generated output contains the exact error key ErrResourceGroupsLoading and the
original message text.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc2afdab-ade5-4c10-9f3d-4835e8c36d16

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8a5f6 and 23a9e24.

📒 Files selected for processing (8)
  • errors.toml
  • pkg/errs/errno.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
  • pkg/mcs/resourcemanager/server/manager_test.go
  • pkg/mcs/resourcemanager/server/metrics.go
  • pkg/storage/endpoint/resource_group.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go

Comment thread pkg/errs/errno.go Outdated
Comment thread pkg/mcs/resourcemanager/server/manager.go
Comment thread pkg/mcs/resourcemanager/server/metrics.go
Comment thread pkg/mcs/resourcemanager/server/metrics.go
Comment thread tests/integrations/mcs/resourcemanager/resource_manager_test.go
Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

Caution

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

⚠️ Outside diff range comments (1)
tests/integrations/mcs/resourcemanager/resource_manager_test.go (1)

276-280: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a cancelable context with per-attempt timeout in async-load polling.

cli.ListResourceGroups(context.TODO()) in the retry loop can block indefinitely and stall Eventually if the RPC hangs. Pass a parent context into this helper and wrap each poll with context.WithTimeout(...).

As per coding guidelines, "Use context-aware timeouts and backoff for retries".

Suggested patch
-func waitAsyncLoadResourceGroups(re *require.Assertions, cli pd.Client) {
+func waitAsyncLoadResourceGroups(ctx context.Context, re *require.Assertions, cli pd.Client) {
 	testutil.Eventually(re, func() bool {
-		_, err := cli.ListResourceGroups(context.TODO())
+		reqCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
+		defer cancel()
+		_, err := cli.ListResourceGroups(reqCtx)
 		return err == nil
 	}, testutil.WithTickInterval(100*time.Millisecond))
 }
-waitAsyncLoadResourceGroups(re, suite.client)
+waitAsyncLoadResourceGroups(suite.ctx, re, suite.client)
-waitAsyncLoadResourceGroups(re, suite.client)
+waitAsyncLoadResourceGroups(suite.ctx, re, suite.client)
🤖 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 `@tests/integrations/mcs/resourcemanager/resource_manager_test.go` around lines
276 - 280, The helper waitAsyncLoadResourceGroups currently calls
cli.ListResourceGroups(context.TODO()) which can block; change the signature to
accept a parent context (e.g., ctx context.Context) and inside the
testutil.Eventually loop wrap each call with a per-attempt timeout using ctx,
e.g., ctxAttempt, cancel := context.WithTimeout(ctx, <short-duration>) and defer
cancel() before calling cli.ListResourceGroups(ctxAttempt) so each RPC is
bounded and won't stall the Eventually loop. Ensure you propagate the parent
context from the test caller into waitAsyncLoadResourceGroups and cancel
per-attempt contexts promptly.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@tests/integrations/mcs/resourcemanager/resource_manager_test.go`:
- Around line 276-280: The helper waitAsyncLoadResourceGroups currently calls
cli.ListResourceGroups(context.TODO()) which can block; change the signature to
accept a parent context (e.g., ctx context.Context) and inside the
testutil.Eventually loop wrap each call with a per-attempt timeout using ctx,
e.g., ctxAttempt, cancel := context.WithTimeout(ctx, <short-duration>) and defer
cancel() before calling cli.ListResourceGroups(ctxAttempt) so each RPC is
bounded and won't stall the Eventually loop. Ensure you propagate the parent
context from the test caller into waitAsyncLoadResourceGroups and cancel
per-attempt contexts promptly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e7ed95a-0515-4118-98e1-ce1afc606568

📥 Commits

Reviewing files that changed from the base of the PR and between 23a9e24 and 966d279.

📒 Files selected for processing (5)
  • errors.toml
  • pkg/errs/errno.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/metrics.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • errors.toml

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

🤖 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/mcs/resourcemanager/server/manager_async_test.go`:
- Around line 49-53: The async loader's teardown can hang because s.once.Do
blocks on `<-s.release` if tests abort without calling store.unblock(); after
creating the test store (the variable named "store"), immediately add `defer
store.unblock()` in each test that uses the async manager so the unblock always
runs even on early returns—apply this fix to the other test locations that
create `store` (the other occurrences referenced around the s.once.Do block and
lines noted) to prevent stopAsyncTestManager (which waits on m.wg.Wait) from
deadlocking.

In `@pkg/mcs/resourcemanager/server/manager.go`:
- Around line 776-778: In AddResourceGroup, don’t unconditionally swallow errors
from m.loadResourceGroupIfNeeded; change the error handling so that after
calling m.loadResourceGroupIfNeeded(keyspaceID, grouppb.Name) you only ignore
the error when it is an explicit “not found” case and return any other error to
the caller. Concretely, inside AddResourceGroup check err from
m.loadResourceGroupIfNeeded and if it is not nil and not a NotFound/IsNotExist
style error (use the existing project helper or error type used elsewhere for
not-found checks), return that err; otherwise proceed and keep the existing
debug log for the not-found path. Ensure you reference the same function names
(m.loadResourceGroupIfNeeded and AddResourceGroup) so the change is applied in
the right place.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3834e145-9204-4a8d-b7bf-66b7bb2e66c6

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8a5f6 and 966d279.

📒 Files selected for processing (8)
  • errors.toml
  • pkg/errs/errno.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
  • pkg/mcs/resourcemanager/server/manager_test.go
  • pkg/mcs/resourcemanager/server/metrics.go
  • pkg/storage/endpoint/resource_group.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go

Comment thread pkg/mcs/resourcemanager/server/manager_async_test.go
Comment thread pkg/mcs/resourcemanager/server/manager.go Outdated
Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

Caution

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

⚠️ Outside diff range comments (1)
tests/integrations/mcs/resourcemanager/resource_manager_test.go (1)

278-283: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pass context as a parameter instead of using context.TODO().

Line 280 uses context.TODO() for the ListResourceGroups RPC call. The coding guideline requires that the first parameter must be context.Context for external effects. The helper should accept a context parameter to respect test timeouts and allow proper cancellation.

🛠️ Suggested fix
-func waitAsyncLoadResourceGroups(re *require.Assertions, cli pd.Client) {
+func waitAsyncLoadResourceGroups(re *require.Assertions, cli pd.Client, ctx context.Context) {
 	testutil.Eventually(re, func() bool {
-		_, err := cli.ListResourceGroups(context.TODO())
+		_, err := cli.ListResourceGroups(ctx)
 		return err == nil
 	}, testutil.WithTickInterval(100*time.Millisecond))
 }

Then update the call sites at lines 199 and 559 to pass suite.ctx or the appropriate context.

🤖 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 `@tests/integrations/mcs/resourcemanager/resource_manager_test.go` around lines
278 - 283, Update the helper waitAsyncLoadResourceGroups to accept a
context.Context argument and use that context when calling
pd.Client.ListResourceGroups instead of context.TODO(); change the function
signature (waitAsyncLoadResourceGroups) and its internal call to use the passed
ctx, then update its call sites (the places that currently call
waitAsyncLoadResourceGroups at the two test locations) to pass suite.ctx (or the
appropriate test context) so the RPC honors test timeouts and cancellations.

Source: Coding guidelines

🧹 Nitpick comments (1)
tests/integrations/mcs/resourcemanager/resource_manager_test.go (1)

1507-1515: 💤 Low value

Consider caching the normalized result to avoid redundant computation.

Lines 1512 and 1514 both call normalizeResourceGroupsForSettingsCompare(newGroups). While functionally correct, this duplicates work. You could compute and store the normalized newGroups once inside the Eventually callback and reuse it in the final assertion for slightly better efficiency.

♻️ Optional refactor
 	expectedGroups := normalizeResourceGroupsForSettingsCompare(groups)
-	var newGroups []*rmpb.ResourceGroup
+	var newGroups, normalizedNewGroups []*rmpb.ResourceGroup
 	testutil.Eventually(re, func() bool {
 		var err error
 		newGroups, err = cli.ListResourceGroups(suite.ctx)
-		return err == nil && reflect.DeepEqual(expectedGroups, normalizeResourceGroupsForSettingsCompare(newGroups))
+		if err != nil {
+			return false
+		}
+		normalizedNewGroups = normalizeResourceGroupsForSettingsCompare(newGroups)
+		return reflect.DeepEqual(expectedGroups, normalizedNewGroups)
 	})
-	re.Equal(expectedGroups, normalizeResourceGroupsForSettingsCompare(newGroups))
+	re.Equal(expectedGroups, normalizedNewGroups)
 }
🤖 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 `@tests/integrations/mcs/resourcemanager/resource_manager_test.go` around lines
1507 - 1515, The test redundantly calls
normalizeResourceGroupsForSettingsCompare(newGroups) twice; modify the
Eventually callback to compute normalizedNew :=
normalizeResourceGroupsForSettingsCompare(newGroups) once (after
cli.ListResourceGroups returns) and return err==nil &&
reflect.DeepEqual(expectedGroups, normalizedNew), then use the same
normalizedNew for the final assertion (re.Equal(expectedGroups, normalizedNew));
this uses the existing variables newGroups and expectedGroups and keeps the same
test semantics while avoiding duplicate normalization.
🤖 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.

Outside diff comments:
In `@tests/integrations/mcs/resourcemanager/resource_manager_test.go`:
- Around line 278-283: Update the helper waitAsyncLoadResourceGroups to accept a
context.Context argument and use that context when calling
pd.Client.ListResourceGroups instead of context.TODO(); change the function
signature (waitAsyncLoadResourceGroups) and its internal call to use the passed
ctx, then update its call sites (the places that currently call
waitAsyncLoadResourceGroups at the two test locations) to pass suite.ctx (or the
appropriate test context) so the RPC honors test timeouts and cancellations.

---

Nitpick comments:
In `@tests/integrations/mcs/resourcemanager/resource_manager_test.go`:
- Around line 1507-1515: The test redundantly calls
normalizeResourceGroupsForSettingsCompare(newGroups) twice; modify the
Eventually callback to compute normalizedNew :=
normalizeResourceGroupsForSettingsCompare(newGroups) once (after
cli.ListResourceGroups returns) and return err==nil &&
reflect.DeepEqual(expectedGroups, normalizedNew), then use the same
normalizedNew for the final assertion (re.Equal(expectedGroups, normalizedNew));
this uses the existing variables newGroups and expectedGroups and keeps the same
test semantics while avoiding duplicate normalization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 866d2ec9-b8ef-4d4e-b3c6-2c05d185d068

📥 Commits

Reviewing files that changed from the base of the PR and between 966d279 and fae9a3e.

📒 Files selected for processing (2)
  • pkg/mcs/resourcemanager/server/manager.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/resourcemanager/server/manager.go

Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

♻️ Duplicate comments (1)
pkg/mcs/resourcemanager/server/manager_async_test.go (1)

35-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make async-block teardown failure-safe to prevent test hangs.

If a test exits before reaching Line 113 / Line 137, the loader can stay blocked on <-s.release and stopAsyncTestManager (Line 85-90) can block on m.wg.Wait().

Proposed minimal fix
 type blockingResourceGroupStorage struct {
 	storage.Storage

 	once    sync.Once
 	entered chan struct{}
 	release chan struct{}
+	releaseOnce sync.Once
 }
@@
 func (s *blockingResourceGroupStorage) unblock() {
-	close(s.release)
+	s.releaseOnce.Do(func() { close(s.release) })
 }
@@
 func TestAsyncLoadResourceGroupsLazyGet(t *testing.T) {
 	re := require.New(t)
 	store := newBlockingResourceGroupStorage()
+	defer store.unblock()
 	re.NoError(store.SaveResourceGroupSetting(1, "lazy-group", newAsyncTestGroup("lazy-group", 100)))
@@
 func TestAsyncLoadResourceGroupsDoesNotRestoreDeletedLazyGroup(t *testing.T) {
 	re := require.New(t)
 	store := newBlockingResourceGroupStorage()
+	defer store.unblock()
 	re.NoError(store.SaveResourceGroupSetting(1, "deleted-group", newAsyncTestGroup("deleted-group", 100)))

As per coding guidelines, “Prevent goroutine leaks: pair with cancellation; consider errgroup” and “Cancel timers/tickers; close resources with defer and error checks.”

Also applies to: 65-67, 94-100, 122-129

Source: Coding guidelines

🧹 Nitpick comments (1)
pkg/errs/errno.go (1)

544-544: ⚡ Quick win

Add GoDoc for ErrResourceGroupsLoading.

This is a new exported error, so it should carry a comment starting with ErrResourceGroupsLoading like the other documented exported identifiers in this file.

As per coding guidelines, "Exported identifiers need GoDoc starting with the name."

🤖 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/errs/errno.go` at line 544, Add a GoDoc comment for the exported variable
ErrResourceGroupsLoading that begins with the identifier name (e.g.,
"ErrResourceGroupsLoading ...") and briefly describes the error meaning
("resource groups are still being loaded, please try again later") and context
(used when the resource manager hasn't finished loading groups). Follow the
style and placement of other documented exported errors in pkg/errs/errno.go so
the comment sits immediately above the ErrResourceGroupsLoading declaration.

Source: Coding guidelines

🤖 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/mcs/resourcemanager/server/manager.go`:
- Around line 581-583: The fast-path currently returns a synthetic in-memory
default by calling m.getOrCreateKeyspaceResourceGroupManager(keyspaceID, true)
when name == DefaultResourceGroupName; change this to first attempt loading the
single persisted group for the default from storage (the same path used by async
merge/load), and only if that storage call returns a not-found should you create
the synthetic reserved group; ensure Get/Modify/ModifyResourceGroup then operate
against the real persisted metadata when present and mark sync-loaded only when
a persisted record actually exists rather than always synthesizing it.

In `@pkg/storage/endpoint/resource_group.go`:
- Around line 78-80: LoadResourceGroupSetting (and the analogous single-item
loader for state) currently only reads the keyspace-scoped path and can miss
legacy entries under the null/legacy keyspace; update
StorageEndpoint.LoadResourceGroupSetting and the corresponding
LoadResourceGroupState function to attempt the keyspace-specific Load first and
if that returns not-found (or empty), fall back to the legacy null keyspace path
(use constant.NullKeyspaceID with keypath.KeyspaceResourceGroupSettingPath /
KeyspaceResourceGroupStatePath and the same name) and return that value so
single-item lookups mirror the bulk loaders' legacy fallback behavior.

---

Nitpick comments:
In `@pkg/errs/errno.go`:
- Line 544: Add a GoDoc comment for the exported variable
ErrResourceGroupsLoading that begins with the identifier name (e.g.,
"ErrResourceGroupsLoading ...") and briefly describes the error meaning
("resource groups are still being loaded, please try again later") and context
(used when the resource manager hasn't finished loading groups). Follow the
style and placement of other documented exported errors in pkg/errs/errno.go so
the comment sits immediately above the ErrResourceGroupsLoading declaration.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f085aac-838e-4a9a-a173-92aaa52b6a05

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8a5f6 and f7cf2cd.

📒 Files selected for processing (8)
  • errors.toml
  • pkg/errs/errno.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
  • pkg/mcs/resourcemanager/server/manager_test.go
  • pkg/mcs/resourcemanager/server/metrics.go
  • pkg/storage/endpoint/resource_group.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go

Comment thread pkg/mcs/resourcemanager/server/manager.go Outdated
Comment on lines +78 to +80
// LoadResourceGroupSetting loads a specific resource group from storage.
func (se *StorageEndpoint) LoadResourceGroupSetting(keyspaceID uint32, name string) (string, error) {
return se.Load(keypath.KeyspaceResourceGroupSettingPath(keyspaceID, name))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mirror the legacy-path fallback in the new single-item loaders.

LoadResourceGroupSettings and LoadResourceGroupStates still hydrate legacy resource_group/* entries as constant.NullKeyspaceID, but these new point-lookups only hit the keyspace-scoped paths. During async bootstrap, lazy Get/Modify/Delete on legacy groups will therefore miss persisted settings or state until the full scan completes.

Please make the single-item helpers follow the same compatibility rule as the bulk loaders, at least for the null/legacy keyspace path.

Also applies to: 112-114

🤖 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/storage/endpoint/resource_group.go` around lines 78 - 80,
LoadResourceGroupSetting (and the analogous single-item loader for state)
currently only reads the keyspace-scoped path and can miss legacy entries under
the null/legacy keyspace; update StorageEndpoint.LoadResourceGroupSetting and
the corresponding LoadResourceGroupState function to attempt the
keyspace-specific Load first and if that returns not-found (or empty), fall back
to the legacy null keyspace path (use constant.NullKeyspaceID with
keypath.KeyspaceResourceGroupSettingPath / KeyspaceResourceGroupStatePath and
the same name) and return that value so single-item lookups mirror the bulk
loaders' legacy fallback behavior.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.68354% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.23%. Comparing base (c2a47d8) to head (8f743d9).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master   #10873    +/-   ##
========================================
  Coverage   79.22%   79.23%            
========================================
  Files         541      541            
  Lines       75965    76197   +232     
========================================
+ Hits        60187    60374   +187     
- Misses      11531    11567    +36     
- Partials     4247     4256     +9     
Flag Coverage Δ
unittests 79.23% <74.68%> (+<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.

@bufferflies

Copy link
Copy Markdown
Contributor Author

/test pull-unit-test-next-gen-2
/test pull-unit-test-next-gen-3

@bufferflies

Copy link
Copy Markdown
Contributor Author

/test pull-unit-test-next-gen-3

}
}
if name == DefaultResourceGroupName {
m.getOrCreateKeyspaceResourceGroupManager(keyspaceID, true)

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 path can create and persist a synthetic default group while async loading is still in progress. If storage already contains a customized default group, an early read or modify can observe or write the built-in defaults instead, and the async merge may not restore the original settings depending on timing.

@rleungx

rleungx commented Jul 7, 2026

Copy link
Copy Markdown
Member

AcquireTokenBuckets still bypasses the new lazy-load path. It calls accessKeyspaceResourceGroupManager before GetMutableResourceGroup, so a persisted non-default group whose keyspace manager is not in memory yet is skipped until the full async load completes. That means token requests can fail during startup even though single-group lazy loading was added for this period.

Address PR tikv#10873 review comments:

- grpc_service: AcquireTokenBuckets checked accessKeyspaceResourceGroupManager
  before GetMutableResourceGroup, bypassing the lazy-load path for non-default
  groups whose keyspace manager wasn't in memory yet. Reorder so the lazy load
  runs first.
- manager: loadResourceGroupIfNeeded synthesized the reserved default group
  unconditionally, which could persist synthetic defaults over a customized
  one still on disk. Try the storage load first and only fall back to the
  synthetic default on an explicit not-found error.
- manager: AddResourceGroup silently swallowed all loadResourceGroupIfNeeded
  errors, including transient storage failures. Only ignore the expected
  not-found case and propagate others.
- manager_async_test: blockingResourceGroupStorage.unblock could hang
  stopAsyncTestManager's wg.Wait() if a test aborted before calling it. Make
  unblock idempotent and defer it right after Init so teardown can't deadlock.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/mcs/resourcemanager/server/grpc_service.go`:
- Around line 237-240: The `GetMutableResourceGroup` handling in
`grpc_service.go` treats every `nil, err` as a missing group and continues,
which hides real load failures. Update the stream handling around
`s.manager.GetMutableResourceGroup` to inspect the returned error: only
`continue` when it is `errs.ErrResourceGroupNotExists`, and for any other error
return it so the caller can fail or retry. Keep the existing warning path for
true not-found cases, but do not swallow non-not-found errors.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e08ba469-8b6e-4348-ade4-4d4c9e063644

📥 Commits

Reviewing files that changed from the base of the PR and between f7cf2cd and a20e813.

📒 Files selected for processing (3)
  • pkg/mcs/resourcemanager/server/grpc_service.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/resourcemanager/server/manager.go

Comment thread pkg/mcs/resourcemanager/server/grpc_service.go
GetMutableResourceGroup can return (nil, err) for real failures (e.g.
ErrKeyspaceNotExists, storage errors during lazy load), not just a
missing group. Treating every rg == nil as "not found" silently
dropped token requests on real errors instead of failing the stream.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>
@bufferflies bufferflies requested a review from lhy1024 July 13, 2026 08:00
…mments

# Conflicts:
#	tests/integrations/mcs/resourcemanager/resource_manager_test.go
}
state, err := m.storage.LoadResourceGroupState(keyspaceID, name)
if err != nil {
log.Warn("failed to load resource group state, continuing without state",

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.

When this state read fails, the group is still returned and subsequently recorded in syncLoadedGroups. The async merge then skips the group even if its bulk state read succeeds, so the cached token bucket can remain initialized without its persisted state for the rest of this manager lifetime and may grant fresh initial tokens.

@bufferflies bufferflies requested a review from rleungx July 13, 2026 09:36
…ailure

When loadResourceGroup's LoadResourceGroupState read fails, it logs a
warning and still returns the group using default state. But
loadResourceGroupIfNeeded then unconditionally marked the group as
sync-loaded, which made asyncLoadResourceGroups's merge permanently
skip it, even if the later bulk load successfully read the real
persisted state. The group would keep its default/fresh state (e.g. a
re-initialized token bucket) for the rest of the manager's lifetime.

Only mark the group sync-loaded when its state was actually read, so
the async bulk merge remains free to fill in the correct state
afterward if nothing else has modified the group in the meantime.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>
@disksing

Copy link
Copy Markdown
Contributor

initReservedInCache inserts a synthetic default group before async loading starts. The early cache-hit return in loadResourceGroupIfNeeded therefore still bypasses the persisted point load for default. A customized default group may be served with built-in settings during startup, and a concurrent mutation may prevent the async merge from restoring the persisted settings. We should distinguish a synthetic reserved entry from one loaded from storage.

@disksing disksing self-requested a review July 13, 2026 13:16
…ed data

initReservedInCache installs a synthetic default resource group into
the cache before async loading starts. loadResourceGroupIfNeeded's
early cache-hit check treated any cached entry as already-loaded, so
it never attempted the persisted point load for a keyspace's default
group as long as the synthetic placeholder occupied the slot. A
customized default group would be served with built-in settings for
the whole startup window, and the async bulk merge could be blocked
from correcting it if a concurrent write marked it sync-loaded first.

Track which cache entries are still just placeholders
(keyspaceResourceGroupManager.reservedGroups, guarded by the same lock
as groups) and clear the mark on any real write. loadResourceGroupIfNeeded
now only treats a cached entry as satisfying the call when it isn't a
placeholder, and is allowed to replace a placeholder with the result
of a real storage load.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>
return errs.ErrKeyspaceNotExists.FastGenByArgs(keyspaceID)
}
return krgm.addResourceGroup(grouppb)
if err := m.loadResourceGroupIfNeeded(keyspaceID, grouppb.Name); err != nil &&

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.

During async loading, adding a resource group can persist a synthetic default group before the customized default is loaded, overwriting its stored settings.

}

m.wg.Add(1)
go m.asyncLoadResourceGroups(ctx)

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 persistence loop can run before async loading completes and overwrite the stored default-group state with the placeholder's fresh state.

krgm = m.getOrCreateKeyspaceResourceGroupManager(keyspaceID, false)
inserted := false
krgm.Lock()
if _, exists := krgm.groups[name]; !exists {

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.

A lazy load that starts before a concurrent Delete can reinsert the deleted group here. The point read happens outside the group lock, so if Delete finishes after the read but before this insertion, the stale result observes an empty cache and inserts the group again. Because the bulk scan no longer contains the deleted entry, it remains visible after loading completes.

if err := krgm.modifyResourceGroup(grouppb); err != nil {
return err
}
m.markResourceGroupSyncLoaded(keyspaceID, grouppb.Name)

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.

If the preceding lazy load continues after LoadResourceGroupState fails, this call marks the group as fully sync-loaded. The bulk merge then skips it, so the persisted running state is never applied and the cached token bucket can remain initialized with empty state.

@disksing

Copy link
Copy Markdown
Contributor

Thanks, 306fd57c addresses the synthetic-default cache-hit/settings issue: a reserved placeholder is no longer treated as confirmed data and can be replaced by a successful storage load. I still see three correctness gaps before this is safe to approve:

  1. persistLoop starts while async loading is in progress, and persistResourceGroupRunningState persists every entry in krgm.groups without checking reservedGroups. The synthetic default can therefore write its fresh token state back to storage before the real state is loaded, so the later load may only recover the already-overwritten value. Please skip reserved entries (or gate persistence until they are confirmed) and add a blocked-load persistence regression test.
  2. When LoadResourceGroupState returns an error, loadResourceGroup still returns the metadata-only group with a nil error. It is inserted into the live manager and the reserved marker is cleared, so Get/Acquire can use a fresh/default token bucket before the bulk load wins; an early mutation can also make that partial entry authoritative again. Please propagate real state-read errors and add failure/recovery coverage for Get/Acquire/Modify.
  3. The point loaders still only read keyspace-scoped paths, while the bulk loaders also read the legacy resource_group/* paths for NullKeyspaceID. Lazy access can therefore report a legacy group as missing and a subsequent mutation can suppress the bulk value. Please mirror the legacy fallback in both point loaders and add setting/state upgrade tests.

The new placeholder tracking itself also needs deterministic coverage for customized persisted defaults and the persistence-before-load case.

…served marker

persistResourceGroupRunningState persisted every entry in krgm.groups
regardless of reservedGroups, so the persist loop (running concurrently
with async loading) could write a synthetic default's fresh token
state back to storage before the real persisted state was ever loaded,
permanently overwriting it. Skip reserved entries there.

Also fix an oversight in the previous placeholder-tracking commit:
loadResourceGroupIfNeeded cleared a group's reserved mark unconditionally
after a lazy load, even when LoadResourceGroupState had failed and the
group only carries default state. Only clear the mark when the state
was actually read, otherwise keep it reserved so a later call or the
async bulk merge can still fill in the real state instead of the
partial result being treated as final.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>
@disksing

Copy link
Copy Markdown
Contributor

Skipping reserved placeholders in the persistence loop fixes the overwrite path, but the other two correctness issues remain.

When LoadResourceGroupState fails, loadResourceGroup still returns a metadata-only group with a fresh/default state and no error. loadResourceGroupIfNeeded then inserts it, allowing the current Get, Modify, or token request to consume that partial group. Keeping it reserved only causes a later request to retry. Please propagate the state-read error, or otherwise complete the state load before exposing the group, and add a deterministic failure-and-recovery test.

The point setting/state loaders also still lack the legacy null-keyspace fallback used by the bulk loaders, so lazy access during startup can incorrectly treat a legacy group as missing. Please make the point and bulk loading paths compatible and add legacy-data upgrade coverage. The latest commit does not add tests for these paths.

getOrCreateKeyspaceResourceGroupManager(id, true) is used by both
AddResourceGroup and SetKeyspaceServiceLimit to make sure a keyspace's
default resource group exists. It called initDefaultResourceGroup
directly, which persists a synthetic default whenever one isn't cached
yet, with no attempt to check storage first. For a keyspace touched for
the first time while async loading is still in progress, this could
overwrite a customized default group's stored settings before the real
data had a chance to load - the same class of bug fixed earlier in
loadResourceGroupIfNeeded, reachable via two more entry points.

Make initDefault=true go through loadResourceGroupIfNeeded (storage
point load first, synthesize only on confirmed not-found) while async
loading is in progress. Once loading has completed, any default still
missing from the cache is confirmed absent from storage, so it's
synthesized directly as before. loadResourceGroupIfNeeded's own
not-found fallback now calls initDefaultResourceGroup directly instead
of going through getOrCreateKeyspaceResourceGroupManager(id, true), to
avoid recursing back into itself.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>
krgm.modifyResourceGroup unconditionally cleared reservedGroups after
patching settings, and Manager.ModifyResourceGroup unconditionally
called markResourceGroupSyncLoaded afterwards. Modify only patches
settings, it never loads or establishes a group's state, so if the
preceding lazy load had failed to read the persisted state
(stateLoaded=false), a Modify call would incorrectly promote the entry
to fully confirmed at both the keyspace-manager level (reservedGroups)
and the manager level (syncLoadedGroups). The async bulk merge would
then skip it forever, so the real persisted running state would never
get applied and the cached token bucket could remain initialized with
empty state indefinitely.

Leave reservedGroups untouched in modifyResourceGroup, and only mark
the group sync-loaded in ModifyResourceGroup when it isn't still
reserved. This lets the existing retry-on-next-access path (and the
async bulk merge) keep trying to fill in the real state instead of the
settings-only patch being treated as a full confirmation.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>
Review feedback questioned whether the point loaders
(LoadResourceGroupSetting/LoadResourceGroupState) miss the legacy,
pre-keyspace resource_group/* path that the bulk loaders fall back to
for constant.NullKeyspaceID. They don't: KeyspaceResourceGroupSettingPath
and KeyspaceResourceGroupStatePath already resolve to the legacy path
for NullKeyspaceID, the same helper both point and bulk loaders use.

Add a regression test exercising the actual lazy-load path
(GetResourceGroup during blocked async loading) against a legacy group
saved under NullKeyspaceID, to make this guarantee explicit and catch
any future divergence between the point and bulk loaders.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
Signed-off-by: tongjian <1045931706@qq.com>

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

🧹 Nitpick comments (1)
pkg/mcs/resourcemanager/server/manager_test.go (1)

497-501: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Prevent potential test deadlocks by using a read timeout.

Reading directly from a channel can hang the test runner indefinitely if dispatchConsumption ever fails to send an item due to a bug or future regression. Wrapping the read in a select block with a timeout ensures the test fails gracefully and quickly.

💡 Proposed fix to add a read timeout
-	item := <-m.consumptionDispatcher
-	re.Equal(uint32(42), item.keyspaceID)
-	re.Equal(req.GetResourceGroupName(), item.resourceGroupName)
-	re.Equal(req.GetConsumptionSinceLastRequest(), item.Consumption)
+	select {
+	case item := <-m.consumptionDispatcher:
+		re.Equal(uint32(42), item.keyspaceID)
+		re.Equal(req.GetResourceGroupName(), item.resourceGroupName)
+		re.Equal(req.GetConsumptionSinceLastRequest(), item.Consumption)
+	case <-time.After(5 * time.Second):
+		re.FailNow("timeout waiting for consumption dispatcher item")
+	}
🤖 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/mcs/resourcemanager/server/manager_test.go` around lines 497 - 501,
Update the channel read in the consumption dispatch test around
consumptionDispatcher to use a select with a bounded timeout, failing the test
clearly if no item arrives; preserve the existing assertions for keyspaceID,
resourceGroupName, and Consumption after receiving the item.
🤖 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/mcs/resourcemanager/server/manager_test.go`:
- Around line 497-501: Update the channel read in the consumption dispatch test
around consumptionDispatcher to use a select with a bounded timeout, failing the
test clearly if no item arrives; preserve the existing assertions for
keyspaceID, resourceGroupName, and Consumption after receiving the item.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 396cd0f7-46db-4e91-9be2-bc164f1576a3

📥 Commits

Reviewing files that changed from the base of the PR and between f7cf2cd and eb3e774.

📒 Files selected for processing (7)
  • pkg/mcs/resourcemanager/server/grpc_service.go
  • pkg/mcs/resourcemanager/server/keyspace_manager.go
  • pkg/mcs/resourcemanager/server/manager.go
  • pkg/mcs/resourcemanager/server/manager_async_test.go
  • pkg/mcs/resourcemanager/server/manager_test.go
  • pkg/mcs/resourcemanager/server/metrics.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/mcs/resourcemanager/server/metrics.go
  • tests/integrations/mcs/resourcemanager/resource_manager_test.go
  • pkg/mcs/resourcemanager/server/manager.go

@bufferflies

Copy link
Copy Markdown
Contributor Author

Re: the point-loader legacy-path concern — I dug into this and don't think it's a bug.

LoadResourceGroupSetting/LoadResourceGroupState build their key via keypath.KeyspaceResourceGroupSettingPath/KeyspaceResourceGroupStatePath, and those helpers already special-case keyspaceID == constant.NullKeyspaceID by resolving to the legacy resource_group/settings|states/{name} path — the exact same helper the bulk loaders (LoadResourceGroupSettings/LoadResourceGroupStates) use for their NullKeyspaceID scan. So a point lookup with NullKeyspaceID and a bulk scan always resolve to the identical physical key for the same group name; there's no separate "legacy fallback" the point loaders are missing, they share the one code path with the bulk loaders.

Added eb3e77403 (TestAsyncLoadResourceGroupsLazyGetLegacyKeyspace) to make this explicit: it saves a group under NullKeyspaceID, blocks the async bulk load, and does a lazy GetResourceGroup(NullKeyspaceID, ...) against it — confirming the lazy path finds it correctly. Happy to extend this if you had a different scenario in mind where the two paths would diverge.

@disksing

Copy link
Copy Markdown
Contributor

You're right about the legacy path: both point loaders already use the same NullKeyspaceID keypath helpers as the bulk loaders, so I withdraw that fallback concern. The new lazy-get test makes the setting lookup explicit.

The state-read issue still blocks approval, though. loadResourceGroup still converts a LoadResourceGroupState error into a metadata-only group with a nil error, so the current Get/Modify/token request can consume fresh state; keeping the entry reserved only helps a later attempt. Please propagate that error (or otherwise complete the state load before returning success) and add deterministic failure/recovery coverage.

There is also a follow-on issue in the recovery path: when the async bulk merge later installs the fully loaded group, it replaces krgm.groups[name] but never removes reservedGroups[name]. After loading completes, the lazy path no longer revisits it, Modify deliberately preserves the marker, and the persist loop keeps skipping it. Please clear the marker when bulk merge installs confirmed data and test that state persistence resumes after a point-load failure followed by successful bulk recovery.

…rmed data

asyncLoadResourceGroups's merge loop installed the fully-loaded
(settings and state) confirmed group into krgm.groups but never
cleared reservedGroups for it. A group that got marked reserved by an
earlier lazy-load state-read failure would stay reserved forever even
after the bulk load correctly recovered it, permanently excluding it
from the state persist loop and from loadResourceGroupIfNeeded's fast
path.

Clear the reserved marker in the same merge step that installs the
confirmed data. Add a regression test that injects a one-time
LoadResourceGroupState failure during a lazy Get, then lets the async
bulk load complete, and asserts the reserved marker is cleared once
the confirmed data lands; verified the test fails without the fix.

Signed-off-by: tongjian <doufuxiaowangzi@gmail.com>
@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@bufferflies: 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 3e6fb18 link true /test pull-error-log-review
pull-unit-test-next-gen-3 3e6fb18 link true /test pull-unit-test-next-gen-3
pull-unit-test-next-gen-2 3e6fb18 link true /test pull-unit-test-next-gen-2

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

dco-signoff: yes Indicates the PR's author has signed the dco. release-note-none Denotes a PR that doesn't merit a release note. 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.

da1b8ba1 implement async loading for resource groups

3 participants