Repository navigation
importinto: fix cancel race before DXF task commit (#69968) - #69991
Conversation
Signed-off-by: ti-chi-bot <ti-community-prow-bot@tidb.io>
|
@D3Hunter This PR has conflicts, I have hold it. |
|
@ti-chi-bot: ## If you want to know how to resolve it, please read the guide in TiDB Dev Guide. DetailsInstructions 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 ti-community-infra/tichi repository. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughIMPORT INTO cancellation now probes live and historical DXF tasks before cancelling and waiting, or directly cancels dangling jobs when no task is found. Tests cover archived-task and lookup-miss races, while documentation describes the updated semantics. ChangesIMPORT INTO cancellation flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ImportExecutor
participant DXFStorage
participant ImportJobStorage
Client->>ImportExecutor: Cancel import job
ImportExecutor->>DXFStorage: Probe live/history task by key
alt task exists
ImportExecutor->>DXFStorage: Cancel task
ImportExecutor->>DXFStorage: Wait for task completion
else task not found
ImportExecutor->>ImportJobStorage: Cancel dangling import job
end
ImportExecutor-->>Client: Return cancellation result
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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/dxf/importinto/job_testkit_test.go`:
- Around line 202-304: Remove the unresolved merge-conflict markers surrounding
the three added subtests in the test file, including the empty HEAD section and
the separator/commit marker. Preserve all three subtests unchanged so the file
compiles and retains the incoming test coverage.
🪄 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 UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c355e5c3-6d95-4214-a802-7dd775737fee
📒 Files selected for processing (7)
.agents/skills/tidb-test-guidelines/references/dxf-case-map.md.agents/skills/tidb-test-guidelines/references/executor-case-map.mdpkg/dxf/importinto/job_doc.gopkg/dxf/importinto/job_testkit_test.gopkg/executor/import_into.gopkg/executor/import_into_test.gopkg/executor/pkg_test.go
| <<<<<<< HEAD | ||
| ======= | ||
|
|
||
| t.Run("cancel user keyspace job with an archived reverted DXF task", func(t *testing.T) { | ||
| jobID, sysKSTaskMgr := setupUserKeyspaceImportJob(t) | ||
| taskID, err := sysKSTaskMgr.CreateTask(ctx, importinto.TaskKey(jobID), proto.ImportInto, "", 1, "", 0, proto.ExtraParams{}, nil) | ||
| require.NoError(t, err) | ||
| // This is a synthetic state, not a normal real-world scheduler outcome: | ||
| // onReverting calls importScheduler.OnDone to fail or cancel the job before | ||
| // RevertedTask marks the DXF task reverted. Bypassing OnDone here isolates | ||
| // the check that an existing terminal task is not treated as a missing task. | ||
| require.NoError(t, sysKSTaskMgr.RevertTask(ctx, taskID, proto.TaskStatePending, fmt.Errorf("already reverted"))) | ||
| require.NoError(t, sysKSTaskMgr.RevertedTask(ctx, taskID)) | ||
| task, err := sysKSTaskMgr.GetTaskByID(ctx, taskID) | ||
| require.NoError(t, err) | ||
| require.NoError(t, sysKSTaskMgr.TransferTasks2History(ctx, []*proto.Task{task})) | ||
| sysKSTK.MustQuery("select count(1) from mysql.tidb_global_task where id = ?", taskID). | ||
| Check(testkit.Rows("0")) | ||
| sysKSTK.MustQuery("select state from mysql.tidb_global_task_history where id = ?", taskID). | ||
| Check(testkit.Rows(string(proto.TaskStateReverted))) | ||
|
|
||
| userKSTK.MustExec(fmt.Sprintf("cancel import job %d", jobID)) | ||
|
|
||
| userKSTK.MustQuery("select status from mysql.tidb_import_jobs where id = ?", jobID). | ||
| Check(testkit.Rows("pending")) | ||
| sysKSTK.MustQuery("select state from mysql.tidb_global_task_history where id = ?", taskID). | ||
| Check(testkit.Rows(string(proto.TaskStateReverted))) | ||
| }) | ||
|
|
||
| t.Run("cancel pending user keyspace job when task is created after lookup miss", func(t *testing.T) { | ||
| jobID, sysKSTaskMgr := setupUserKeyspaceImportJob(t) | ||
| sysKSTK.MustQuery("select count(1) from mysql.tidb_global_task where task_key = ?", importinto.TaskKey(jobID)). | ||
| Check(testkit.Rows("0")) | ||
|
|
||
| var lateTaskID atomic.Int64 | ||
| callbackErrCh := make(chan error, 1) | ||
| testfailpoint.EnableCall(t, "github.com/pingcap/tidb/pkg/executor/afterCancelImportTaskProbeMiss", | ||
| func(probeMissJobID int64) { | ||
| if probeMissJobID != jobID { | ||
| callbackErrCh <- fmt.Errorf("unexpected job ID %d", probeMissJobID) | ||
| return | ||
| } | ||
| taskID, err := sysKSTaskMgr.CreateTask(ctx, importinto.TaskKey(jobID), proto.ImportInto, "", 1, "", 0, proto.ExtraParams{}, nil) | ||
| if err != nil { | ||
| callbackErrCh <- err | ||
| return | ||
| } | ||
| lateTaskID.Store(taskID) | ||
| }, | ||
| ) | ||
|
|
||
| cancelErrCh := make(chan error, 1) | ||
| go func() { | ||
| _, err := userKSTK.Exec(fmt.Sprintf("cancel import job %d", jobID)) | ||
| cancelErrCh <- err | ||
| }() | ||
|
|
||
| select { | ||
| case err := <-cancelErrCh: | ||
| require.NoError(t, err) | ||
| case <-time.After(5 * time.Second): | ||
| if taskID := lateTaskID.Load(); taskID != 0 { | ||
| require.NoError(t, sysKSTaskMgr.FailTask(ctx, taskID, proto.TaskStatePending, fmt.Errorf("unblock timed out cancel"))) | ||
| <-cancelErrCh | ||
| } | ||
| require.FailNow(t, "cancel import waited for a task created after the cancel-by-key miss") | ||
| } | ||
| select { | ||
| case err := <-callbackErrCh: | ||
| require.NoError(t, err) | ||
| default: | ||
| } | ||
|
|
||
| userKSTK.MustQuery("select status, error_message from mysql.tidb_import_jobs where id = ?", jobID). | ||
| Check(testkit.Rows("cancelled cancelled by user")) | ||
| sysKSTK.MustQuery("select state from mysql.tidb_global_task where task_key = ?", importinto.TaskKey(jobID)). | ||
| Check(testkit.Rows(string(proto.TaskStatePending))) | ||
| }) | ||
|
|
||
| t.Run("submit global-sort task uses async prepare mode", func(t *testing.T) { | ||
| sysKSTK.MustExec("delete from mysql.tidb_import_jobs") | ||
| sysKSTK.MustExec("delete from mysql.tidb_global_task") | ||
| config.UpdateGlobal(func(conf *config.Config) { | ||
| conf.KeyspaceName = keyspace.System | ||
| }) | ||
| manuallyInitFn(t, sysKSStore, sysKSStore) | ||
|
|
||
| jobID, task, err := importinto.SubmitTask(ctx, &importer.Plan{ | ||
| TableInfo: &model.TableInfo{}, | ||
| Parameters: &importer.ImportParameters{}, | ||
| ThreadCnt: 16, | ||
| MaxNodeCnt: 8, | ||
| CloudStorageURI: "local:///tmp/prepare-mode-sort", | ||
| }, "import into t from 's3://bucket/test.csv'") | ||
| require.NoError(t, err) | ||
| sysKSTK.MustQuery("select count(1) from mysql.tidb_import_jobs where id = ?", jobID).Check(testkit.Rows("1")) | ||
| sysKSTK.MustQuery( | ||
| "select concurrency, max_node_count, json_extract(extra_params, '$.prepare_mode') "+ | ||
| "from mysql.tidb_global_task where id = ?", | ||
| task.ID, | ||
| ).Check(testkit.Rows("1 1 1")) | ||
| }) | ||
| >>>>>>> ce53f1c50aa (importinto: fix cancel race before DXF task commit (#69968)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Unresolved merge-conflict markers left in source — file won't compile.
Lines 202-204 and 304 still contain literal <<<<<<< HEAD / ======= / >>>>>>> ce53f1c50aa (...) markers. This breaks go build/go vet for the package. Since the HEAD side is empty, resolution here is just removing the markers and keeping the incoming content (the three new subtests).
🔧 Proposed fix
-<<<<<<< HEAD
-=======
t.Run("cancel user keyspace job with an archived reverted DXF task", func(t *testing.T) {
@@ ... (subtests unchanged) ... @@
- })
->>>>>>> ce53f1c50aa (importinto: fix cancel race before DXF task commit (`#69968`))
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <<<<<<< HEAD | |
| ======= | |
| t.Run("cancel user keyspace job with an archived reverted DXF task", func(t *testing.T) { | |
| jobID, sysKSTaskMgr := setupUserKeyspaceImportJob(t) | |
| taskID, err := sysKSTaskMgr.CreateTask(ctx, importinto.TaskKey(jobID), proto.ImportInto, "", 1, "", 0, proto.ExtraParams{}, nil) | |
| require.NoError(t, err) | |
| // This is a synthetic state, not a normal real-world scheduler outcome: | |
| // onReverting calls importScheduler.OnDone to fail or cancel the job before | |
| // RevertedTask marks the DXF task reverted. Bypassing OnDone here isolates | |
| // the check that an existing terminal task is not treated as a missing task. | |
| require.NoError(t, sysKSTaskMgr.RevertTask(ctx, taskID, proto.TaskStatePending, fmt.Errorf("already reverted"))) | |
| require.NoError(t, sysKSTaskMgr.RevertedTask(ctx, taskID)) | |
| task, err := sysKSTaskMgr.GetTaskByID(ctx, taskID) | |
| require.NoError(t, err) | |
| require.NoError(t, sysKSTaskMgr.TransferTasks2History(ctx, []*proto.Task{task})) | |
| sysKSTK.MustQuery("select count(1) from mysql.tidb_global_task where id = ?", taskID). | |
| Check(testkit.Rows("0")) | |
| sysKSTK.MustQuery("select state from mysql.tidb_global_task_history where id = ?", taskID). | |
| Check(testkit.Rows(string(proto.TaskStateReverted))) | |
| userKSTK.MustExec(fmt.Sprintf("cancel import job %d", jobID)) | |
| userKSTK.MustQuery("select status from mysql.tidb_import_jobs where id = ?", jobID). | |
| Check(testkit.Rows("pending")) | |
| sysKSTK.MustQuery("select state from mysql.tidb_global_task_history where id = ?", taskID). | |
| Check(testkit.Rows(string(proto.TaskStateReverted))) | |
| }) | |
| t.Run("cancel pending user keyspace job when task is created after lookup miss", func(t *testing.T) { | |
| jobID, sysKSTaskMgr := setupUserKeyspaceImportJob(t) | |
| sysKSTK.MustQuery("select count(1) from mysql.tidb_global_task where task_key = ?", importinto.TaskKey(jobID)). | |
| Check(testkit.Rows("0")) | |
| var lateTaskID atomic.Int64 | |
| callbackErrCh := make(chan error, 1) | |
| testfailpoint.EnableCall(t, "github.com/pingcap/tidb/pkg/executor/afterCancelImportTaskProbeMiss", | |
| func(probeMissJobID int64) { | |
| if probeMissJobID != jobID { | |
| callbackErrCh <- fmt.Errorf("unexpected job ID %d", probeMissJobID) | |
| return | |
| } | |
| taskID, err := sysKSTaskMgr.CreateTask(ctx, importinto.TaskKey(jobID), proto.ImportInto, "", 1, "", 0, proto.ExtraParams{}, nil) | |
| if err != nil { | |
| callbackErrCh <- err | |
| return | |
| } | |
| lateTaskID.Store(taskID) | |
| }, | |
| ) | |
| cancelErrCh := make(chan error, 1) | |
| go func() { | |
| _, err := userKSTK.Exec(fmt.Sprintf("cancel import job %d", jobID)) | |
| cancelErrCh <- err | |
| }() | |
| select { | |
| case err := <-cancelErrCh: | |
| require.NoError(t, err) | |
| case <-time.After(5 * time.Second): | |
| if taskID := lateTaskID.Load(); taskID != 0 { | |
| require.NoError(t, sysKSTaskMgr.FailTask(ctx, taskID, proto.TaskStatePending, fmt.Errorf("unblock timed out cancel"))) | |
| <-cancelErrCh | |
| } | |
| require.FailNow(t, "cancel import waited for a task created after the cancel-by-key miss") | |
| } | |
| select { | |
| case err := <-callbackErrCh: | |
| require.NoError(t, err) | |
| default: | |
| } | |
| userKSTK.MustQuery("select status, error_message from mysql.tidb_import_jobs where id = ?", jobID). | |
| Check(testkit.Rows("cancelled cancelled by user")) | |
| sysKSTK.MustQuery("select state from mysql.tidb_global_task where task_key = ?", importinto.TaskKey(jobID)). | |
| Check(testkit.Rows(string(proto.TaskStatePending))) | |
| }) | |
| t.Run("submit global-sort task uses async prepare mode", func(t *testing.T) { | |
| sysKSTK.MustExec("delete from mysql.tidb_import_jobs") | |
| sysKSTK.MustExec("delete from mysql.tidb_global_task") | |
| config.UpdateGlobal(func(conf *config.Config) { | |
| conf.KeyspaceName = keyspace.System | |
| }) | |
| manuallyInitFn(t, sysKSStore, sysKSStore) | |
| jobID, task, err := importinto.SubmitTask(ctx, &importer.Plan{ | |
| TableInfo: &model.TableInfo{}, | |
| Parameters: &importer.ImportParameters{}, | |
| ThreadCnt: 16, | |
| MaxNodeCnt: 8, | |
| CloudStorageURI: "local:///tmp/prepare-mode-sort", | |
| }, "import into t from 's3://bucket/test.csv'") | |
| require.NoError(t, err) | |
| sysKSTK.MustQuery("select count(1) from mysql.tidb_import_jobs where id = ?", jobID).Check(testkit.Rows("1")) | |
| sysKSTK.MustQuery( | |
| "select concurrency, max_node_count, json_extract(extra_params, '$.prepare_mode') "+ | |
| "from mysql.tidb_global_task where id = ?", | |
| task.ID, | |
| ).Check(testkit.Rows("1 1 1")) | |
| }) | |
| >>>>>>> ce53f1c50aa (importinto: fix cancel race before DXF task commit (#69968)) | |
| t.Run("cancel user keyspace job with an archived reverted DXF task", func(t *testing.T) { | |
| jobID, sysKSTaskMgr := setupUserKeyspaceImportJob(t) | |
| taskID, err := sysKSTaskMgr.CreateTask(ctx, importinto.TaskKey(jobID), proto.ImportInto, "", 1, "", 0, proto.ExtraParams{}, nil) | |
| require.NoError(t, err) | |
| // This is a synthetic state, not a normal real-world scheduler outcome: | |
| // onReverting calls importScheduler.OnDone to fail or cancel the job before | |
| // RevertedTask marks the DXF task reverted. Bypassing OnDone here isolates | |
| // the check that an existing terminal task is not treated as a missing task. | |
| require.NoError(t, sysKSTaskMgr.RevertTask(ctx, taskID, proto.TaskStatePending, fmt.Errorf("already reverted"))) | |
| require.NoError(t, sysKSTaskMgr.RevertedTask(ctx, taskID)) | |
| task, err := sysKSTaskMgr.GetTaskByID(ctx, taskID) | |
| require.NoError(t, err) | |
| require.NoError(t, sysKSTaskMgr.TransferTasks2History(ctx, []*proto.Task{task})) | |
| sysKSTK.MustQuery("select count(1) from mysql.tidb_global_task where id = ?", taskID). | |
| Check(testkit.Rows("0")) | |
| sysKSTK.MustQuery("select state from mysql.tidb_global_task_history where id = ?", taskID). | |
| Check(testkit.Rows(string(proto.TaskStateReverted))) | |
| userKSTK.MustExec(fmt.Sprintf("cancel import job %d", jobID)) | |
| userKSTK.MustQuery("select status from mysql.tidb_import_jobs where id = ?", jobID). | |
| Check(testkit.Rows("pending")) | |
| sysKSTK.MustQuery("select state from mysql.tidb_global_task_history where id = ?", taskID). | |
| Check(testkit.Rows(string(proto.TaskStateReverted))) | |
| }) | |
| t.Run("cancel pending user keyspace job when task is created after lookup miss", func(t *testing.T) { | |
| jobID, sysKSTaskMgr := setupUserKeyspaceImportJob(t) | |
| sysKSTK.MustQuery("select count(1) from mysql.tidb_global_task where task_key = ?", importinto.TaskKey(jobID)). | |
| Check(testkit.Rows("0")) | |
| var lateTaskID atomic.Int64 | |
| callbackErrCh := make(chan error, 1) | |
| testfailpoint.EnableCall(t, "github.com/pingcap/tidb/pkg/executor/afterCancelImportTaskProbeMiss", | |
| func(probeMissJobID int64) { | |
| if probeMissJobID != jobID { | |
| callbackErrCh <- fmt.Errorf("unexpected job ID %d", probeMissJobID) | |
| return | |
| } | |
| taskID, err := sysKSTaskMgr.CreateTask(ctx, importinto.TaskKey(jobID), proto.ImportInto, "", 1, "", 0, proto.ExtraParams{}, nil) | |
| if err != nil { | |
| callbackErrCh <- err | |
| return | |
| } | |
| lateTaskID.Store(taskID) | |
| }, | |
| ) | |
| cancelErrCh := make(chan error, 1) | |
| go func() { | |
| _, err := userKSTK.Exec(fmt.Sprintf("cancel import job %d", jobID)) | |
| cancelErrCh <- err | |
| }() | |
| select { | |
| case err := <-cancelErrCh: | |
| require.NoError(t, err) | |
| case <-time.After(5 * time.Second): | |
| if taskID := lateTaskID.Load(); taskID != 0 { | |
| require.NoError(t, sysKSTaskMgr.FailTask(ctx, taskID, proto.TaskStatePending, fmt.Errorf("unblock timed out cancel"))) | |
| <-cancelErrCh | |
| } | |
| require.FailNow(t, "cancel import waited for a task created after the cancel-by-key miss") | |
| } | |
| select { | |
| case err := <-callbackErrCh: | |
| require.NoError(t, err) | |
| default: | |
| } | |
| userKSTK.MustQuery("select status, error_message from mysql.tidb_import_jobs where id = ?", jobID). | |
| Check(testkit.Rows("cancelled cancelled by user")) | |
| sysKSTK.MustQuery("select state from mysql.tidb_global_task where task_key = ?", importinto.TaskKey(jobID)). | |
| Check(testkit.Rows(string(proto.TaskStatePending))) | |
| }) | |
| t.Run("submit global-sort task uses async prepare mode", func(t *testing.T) { | |
| sysKSTK.MustExec("delete from mysql.tidb_import_jobs") | |
| sysKSTK.MustExec("delete from mysql.tidb_global_task") | |
| config.UpdateGlobal(func(conf *config.Config) { | |
| conf.KeyspaceName = keyspace.System | |
| }) | |
| manuallyInitFn(t, sysKSStore, sysKSStore) | |
| jobID, task, err := importinto.SubmitTask(ctx, &importer.Plan{ | |
| TableInfo: &model.TableInfo{}, | |
| Parameters: &importer.ImportParameters{}, | |
| ThreadCnt: 16, | |
| MaxNodeCnt: 8, | |
| CloudStorageURI: "local:///tmp/prepare-mode-sort", | |
| }, "import into t from 's3://bucket/test.csv'") | |
| require.NoError(t, err) | |
| sysKSTK.MustQuery("select count(1) from mysql.tidb_import_jobs where id = ?", jobID).Check(testkit.Rows("1")) | |
| sysKSTK.MustQuery( | |
| "select concurrency, max_node_count, json_extract(extra_params, '$.prepare_mode') "+ | |
| "from mysql.tidb_global_task where id = ?", | |
| task.ID, | |
| ).Check(testkit.Rows("1 1 1")) | |
| }) |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 254-254: Detected a SQL statement built with 'fmt.Sprintf' and passed directly to 'db.Exec'/'db.ExecContext'. Interpolating values into a query string lets an attacker inject arbitrary SQL. Use parameterized queries instead: pass the SQL with placeholders ('?' or '') as the query argument and supply the values as separate arguments, e.g. 'db.Exec("UPDATE t SET x = ? WHERE id = ?", x, id)'.
Context: userKSTK.Exec(fmt.Sprintf("cancel import job %d", jobID))
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
(sql-injection-exec-sprintf-go)
🪛 OpenGrep (1.25.0)
[ERROR] 255-255: SQL query built via fmt.Sprintf or string concatenation passed to a database method. Use parameterized queries with placeholder arguments.
(coderabbit.sql-injection.go-query-format)
🤖 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/dxf/importinto/job_testkit_test.go` around lines 202 - 304, Remove the
unresolved merge-conflict markers surrounding the three added subtests in the
test file, including the empty HEAD section and the separator/commit marker.
Preserve all three subtests unchanged so the file compiles and retains the
incoming test coverage.
Signed-off-by: D3Hunter <jujj603@gmail.com>
|
Cherry-pick conflicts appear resolved; removing the |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## release-nextgen-202603 #69991 +/- ##
===========================================================
Coverage ? 76.1759%
===========================================================
Files ? 1937
Lines ? 541181
Branches ? 0
===========================================================
Hits ? 412250
Misses ? 128931
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Signed-off-by: D3Hunter <jujj603@gmail.com>
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: D3Hunter, GMHDBJD The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
/retest |
b3fa6d7
into
pingcap:release-nextgen-202603
This is an automated cherry-pick of #69968
What problem does this PR solve?
Issue Number: ref #61702
Problem Summary:
This is a follow-up to #69701.
In nextgen user keyspace mode, the import job and its SYSTEM-keyspace DXF task are committed in separate transactions. If
CANCEL IMPORT JOBupdates the DXF task before the task transaction commits, but the task becomes visible beforeWaitTaskDoneByKeyperforms its first lookup, no cancellation is recorded. The command then waits for the task's natural terminal state and can return successfully even though it did not cancel the import job.What changed and how does it work?
Check List
Tests
Side effects
Documentation
Release note
Please refer to Release Notes Language Style Guide to write a quality release note.
Summary by CodeRabbit