Repository: pingcap/tidb
Pull request #48575 (2023-11-14 03:27:49)

*: upgrade and adapt to the new PD client
<!--

Thank you for contributing to TiDB!

PR Title Format:
1. pkg [, pkg2, pkg3]: what's changed
2. *: what's changed

-->

### What problem does this PR solve?
<!--

Please create an issue first to describe the problem.

There MUST be one line starting with "Issue Number:  " and 
linking the relevant issues via the "close" or "ref".

For more info, check https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/contribute-code.html#referring-to-an-issue.

-->

Issue Number: ref #35319.

Problem Summary: upgrade and adapt to the new PD client.

### What is changed and how it works?

### Check List

Tests <!-- At least one of them must be included. -->

- [x] Unit test
- [x] Integration test
- [x] Manual test (add detailed scripts or steps below)

### Release note

<!-- compatibility change, improvement, bugfix, and new feature need a release note -->

```release-note
None
```


ti-chi-bot[bot]: [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 [dsdashun](https://github.com/dsdashun) for approval. For more information see [the Kubernetes Code Review Process](https://book.prow.tidb.net/#/workflows/pr).

The full list of commands accepted by this bot can be found [here](https://prow.tidb.net/command-help?repo=pingcap%2Ftidb).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/pingcap/tidb/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["dsdashun"]} -->
ti-chi-bot[bot]: @JmPotato: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
idc-jenkins-ci-tidb/unit-test | 5d7a2c14db65dc66395dc81b5e673025280d004c | [link](https://do.pingcap.net/jenkins/job/pingcap/job/tidb/job/ghpr_unit_test/37346/display/redirect) | true | `/test unit-test`

[Full PR test history](https://prow.tidb.net/pr-history?org=pingcap&repo=tidb&pr=48575). [Your PR dashboard](https://prow.tidb.net/pr?query=is%3Apr%20state%3Aopen%20author%3AJmPotato).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/DEPS.bzl b/DEPS.bzl
index 0acaa787ba..792af269c2 100644
--- a/DEPS.bzl
+++ b/DEPS.bzl
@@ -7132,13 +7132,13 @@ def go_deps():
         name = "com_github_tikv_pd_client",
         build_file_proto_mode = "disable_global",
         importpath = "github.com/tikv/pd/client",
-        sha256 = "79a51a978de81c8a893e2b33c279ac84e3f95ccdf70cb7f62fef9a76472cd92b",
-        strip_prefix = "github.com/tikv/pd/client@v0.0.0-20230912103610-2f57a9f050eb",
+        sha256 = "cadd6f9cf411690e66fd2e5bad6ef837dd8522d98ddaf79a6a2f9361cbc558c1",
+        strip_prefix = "github.com/tikv/pd/client@v0.0.0-20231113092444-be31c08186fa",
         urls = [
-            "http://bazel-cache.pingcap.net:8080/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20230912103610-2f57a9f050eb.zip",
-            "http://ats.apps.svc/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20230912103610-2f57a9f050eb.zip",
-            "https://cache.hawkingrei.com/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20230912103610-2f57a9f050eb.zip",
-            "https://storage.googleapis.com/pingcapmirror/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20230912103610-2f57a9f050eb.zip",
+            "http://bazel-cache.pingcap.net:8080/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20231113092444-be31c08186fa.zip",
+            "http://ats.apps.svc/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20231113092444-be31c08186fa.zip",
+            "https://cache.hawkingrei.com/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20231113092444-be31c08186fa.zip",
+            "https://storage.googleapis.com/pingcapmirror/gomod/github.com/tikv/pd/client/com_github_tikv_pd_client-v0.0.0-20231113092444-be31c08186fa.zip",
         ],
     )
     go_repository(
diff --git a/go.mod b/go.mod
index 95012ee847..92d9e353fb 100644
--- a/go.mod
+++ b/go.mod
@@ -103,7 +103,7 @@ require (
 	github.com/tdakkota/asciicheck v0.2.0
 	github.com/tiancaiamao/appdash v0.0.0-20181126055449-889f96f722a2
 	github.com/tikv/client-go/v2 v2.0.8-0.20231030120815-1362f1e87566
-	github.com/tikv/pd/client v0.0.0-20230912103610-2f57a9f050eb
+	github.com/tikv/pd/client v0.0.0-20231113092444-be31c08186fa
 	github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966
 	github.com/twmb/murmur3 v1.1.6
 	github.com/uber/jaeger-client-go v2.22.1+incompatible
diff --git a/go.sum b/go.sum
index e1b62bfb45..97d9542109 100644
--- a/go.sum
+++ b/go.sum
@@ -993,8 +993,8 @@ github.com/tiancaiamao/gp v0.0.0-20221230034425-4025bc8a4d4a h1:J/YdBZ46WKpXsxsW
 github.com/tiancaiamao/gp v0.0.0-20221230034425-4025bc8a4d4a/go.mod h1:h4xBhSNtOeEosLJ4P7JyKXX7Cabg7AVkWCK5gV2vOrM=
 github.com/tikv/client-go/v2 v2.0.8-0.20231030120815-1362f1e87566 h1:ULv8/h2S2daBtNDoovptSBC5fJEBKrx0K7E1K8iVOSw=
 github.com/tikv/client-go/v2 v2.0.8-0.20231030120815-1362f1e87566/go.mod h1:XiEHwWZfJqgafxW/VEgi1ltGWB9yjwCJBs2kW1xHMY4=
-github.com/tikv/pd/client v0.0.0-20230912103610-2f57a9f050eb h1:hAcH9tFjQzQ3+ofrAHm4ajOTLliYCOfXpj3+boKOtac=
-github.com/tikv/pd/client v0.0.0-20230912103610-2f57a9f050eb/go.mod h1:E+6qtPu8fJm5kNjvKWPVFqSgNAFPk07y2EjD03GWzuI=
+github.com/tikv/pd/client v0.0.0-20231113092444-be31c08186fa h1:qgbTvsjSzU2A9ItK+NUSUHgtvDTeaWk1mGH2Kjbaf7s=
+github.com/tikv/pd/client v0.0.0-20231113092444-be31c08186fa/go.mod h1:cd6zBqRM9aogxf26K8NnFRPVtq9BnRE59tKEpX8IaWQ=
 github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M=
 github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ=
 github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
diff --git a/pkg/domain/infosync/resource_manager_client.go b/pkg/domain/infosync/resource_manager_client.go
index b183daebec..3cf1a2e51b 100644
--- a/pkg/domain/infosync/resource_manager_client.go
+++ b/pkg/domain/infosync/resource_manager_client.go
@@ -15,6 +15,7 @@
 package infosync
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"math"
@@ -144,5 +145,8 @@ func (m *mockResourceManagerClient) LoadResourceGroups(ctx context.Context) ([]*
 }
 
 func (m *mockResourceManagerClient) Watch(ctx context.Context, key []byte, opts ...pd.OpOption) (chan []*meta_storagepb.Event, error) {
-	return m.eventCh, nil
+	if bytes.Equal(pd.GroupSettingsPathPrefixBytes, key) {
+		return m.eventCh, nil
+	}
+	return nil, nil
 }
diff --git a/pkg/executor/internal/calibrateresource/BUILD.bazel b/pkg/executor/internal/calibrateresource/BUILD.bazel
index 4091276050..579adff6fc 100644
--- a/pkg/executor/internal/calibrateresource/BUILD.bazel
+++ b/pkg/executor/internal/calibrateresource/BUILD.bazel
@@ -45,6 +45,7 @@ go_test(
         "//pkg/types",
         "@com_github_pingcap_errors//:errors",
         "@com_github_pingcap_failpoint//:failpoint",
+        "@com_github_pingcap_kvproto//pkg/meta_storagepb",
         "@com_github_stretchr_testify//require",
         "@com_github_tikv_client_go_v2//tikv",
         "@com_github_tikv_pd_client//:client",
diff --git a/pkg/executor/sortexec/BUILD.bazel b/pkg/executor/sortexec/BUILD.bazel
index 81d3caed95..a5498953bd 100644
--- a/pkg/executor/sortexec/BUILD.bazel
+++ b/pkg/executor/sortexec/BUILD.bazel
@@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 go_library(
     name = "sortexec",
-    srcs = ["sort.go"],
+    srcs = [
+        "sort.go",
+        "topn.go",
+    ],
     importpath = "github.com/pingcap/tidb/pkg/executor/sortexec",
     visibility = ["//visibility:public"],
     deps = [
diff --git a/pkg/executor/sortexec/sort.go b/pkg/executor/sortexec/sort.go
index 030ec2b426..ce26bac26b 100644
--- a/pkg/executor/sortexec/sort.go
+++ b/pkg/executor/sortexec/sort.go
@@ -18,12 +18,10 @@ import (
 	"container/heap"
 	"context"
 	"errors"
-	"slices"
 
 	"github.com/pingcap/failpoint"
 	"github.com/pingcap/tidb/pkg/executor/internal/exec"
 	"github.com/pingcap/tidb/pkg/expression"
-	plannercore "github.com/pingcap/tidb/pkg/planner/core"
 	"github.com/pingcap/tidb/pkg/planner/util"
 	"github.com/pingcap/tidb/pkg/sessionctx/variable"
 	"github.com/pingcap/tidb/pkg/util/chunk"
@@ -328,223 +326,3 @@ func (h *multiWayMerge) Pop() interface{} {
 func (h *multiWayMerge) Swap(i, j int) {
 	h.elements[i], h.elements[j] = h.elements[j], h.elements[i]
 }
-
-// TopNExec implements a Top-N algorithm and it is built from a SELECT statement with ORDER BY and LIMIT.
-// Instead of sorting all the rows fetched from the table, it keeps the Top-N elements only in a heap to reduce memory usage.
-type TopNExec struct {
-	SortExec
-	Limit      *plannercore.PhysicalLimit
-	totalLimit uint64
-
-	// rowChunks is the chunks to store row values.
-	rowChunks *chunk.List
-	// rowPointer store the chunk index and row index for each row.
-	rowPtrs []chunk.RowPtr
-
-	chkHeap *topNChunkHeap
-}
-
-// topNChunkHeap implements heap.Interface.
-type topNChunkHeap struct {
-	*TopNExec
-}
-
-// Less implement heap.Interface, but since we mantains a max heap,
-// this function returns true if row i is greater than row j.
-func (h *topNChunkHeap) Less(i, j int) bool {
-	rowI := h.rowChunks.GetRow(h.rowPtrs[i])
-	rowJ := h.rowChunks.GetRow(h.rowPtrs[j])
-	return h.greaterRow(rowI, rowJ)
-}
-
-func (h *topNChunkHeap) greaterRow(rowI, rowJ chunk.Row) bool {
-	for i, colIdx := range h.keyColumns {
-		cmpFunc := h.keyCmpFuncs[i]
-		cmp := cmpFunc(rowI, colIdx, rowJ, colIdx)
-		if h.ByItems[i].Desc {
-			cmp = -cmp
-		}
-		if cmp > 0 {
-			return true
-		} else if cmp < 0 {
-			return false
-		}
-	}
-	return false
-}
-
-func (h *topNChunkHeap) Len() int {
-	return len(h.rowPtrs)
-}
-
-func (*topNChunkHeap) Push(interface{}) {
-	// Should never be called.
-}
-
-func (h *topNChunkHeap) Pop() interface{} {
-	h.rowPtrs = h.rowPtrs[:len(h.rowPtrs)-1]
-	// We don't need the popped value, return nil to avoid memory allocation.
-	return nil
-}
-
-func (h *topNChunkHeap) Swap(i, j int) {
-	h.rowPtrs[i], h.rowPtrs[j] = h.rowPtrs[j], h.rowPtrs[i]
-}
-
-// keyColumnsLess is the less function for key columns.
-func (e *TopNExec) keyColumnsLess(i, j chunk.RowPtr) bool {
-	rowI := e.rowChunks.GetRow(i)
-	rowJ := e.rowChunks.GetRow(j)
-	return e.lessRow(rowI, rowJ)
-}
-
-func (e *TopNExec) keyColumnsCompare(i, j chunk.RowPtr) int {
-	rowI := e.rowChunks.GetRow(i)
-	rowJ := e.rowChunks.GetRow(j)
-	return e.compressRow(rowI, rowJ)
-}
-
-func (e *TopNExec) initPointers() {
-	e.rowPtrs = make([]chunk.RowPtr, 0, e.rowChunks.Len())
-	e.memTracker.Consume(int64(8 * e.rowChunks.Len()))
-	for chkIdx := 0; chkIdx < e.rowChunks.NumChunks(); chkIdx++ {
-		rowChk := e.rowChunks.GetChunk(chkIdx)
-		for rowIdx := 0; rowIdx < rowChk.NumRows(); rowIdx++ {
-			e.rowPtrs = append(e.rowPtrs, chunk.RowPtr{ChkIdx: uint32(chkIdx), RowIdx: uint32(rowIdx)})
-		}
-	}
-}
-
-// Open implements the Executor Open interface.
-func (e *TopNExec) Open(ctx context.Context) error {
-	e.memTracker = memory.NewTracker(e.ID(), -1)
-	e.memTracker.AttachTo(e.Ctx().GetSessionVars().StmtCtx.MemTracker)
-
-	e.fetched = false
-	e.Idx = 0
-
-	return exec.Open(ctx, e.Children(0))
-}
-
-// Next implements the Executor Next interface.
-func (e *TopNExec) Next(ctx context.Context, req *chunk.Chunk) error {
-	req.Reset()
-	if !e.fetched {
-		e.totalLimit = e.Limit.Offset + e.Limit.Count
-		e.Idx = int(e.Limit.Offset)
-		err := e.loadChunksUntilTotalLimit(ctx)
-		if err != nil {
-			return err
-		}
-		err = e.executeTopN(ctx)
-		if err != nil {
-			return err
-		}
-		e.fetched = true
-	}
-	if e.Idx >= len(e.rowPtrs) {
-		return nil
-	}
-	if !req.IsFull() {
-		numToAppend := min(len(e.rowPtrs)-e.Idx, req.RequiredRows()-req.NumRows())
-		rows := make([]chunk.Row, numToAppend)
-		for index := 0; index < numToAppend; index++ {
-			rows[index] = e.rowChunks.GetRow(e.rowPtrs[e.Idx])
-			e.Idx++
-		}
-		req.AppendRows(rows)
-	}
-	return nil
-}
-
-func (e *TopNExec) loadChunksUntilTotalLimit(ctx context.Context) error {
-	e.chkHeap = &topNChunkHeap{e}
-	e.rowChunks = chunk.NewList(exec.RetTypes(e), e.InitCap(), e.MaxChunkSize())
-	e.rowChunks.GetMemTracker().AttachTo(e.memTracker)
-	e.rowChunks.GetMemTracker().SetLabel(memory.LabelForRowChunks)
-	for uint64(e.rowChunks.Len()) < e.totalLimit {
-		srcChk := exec.TryNewCacheChunk(e.Children(0))
-		// adjust required rows by total limit
-		srcChk.SetRequiredRows(int(e.totalLimit-uint64(e.rowChunks.Len())), e.MaxChunkSize())
-		err := exec.Next(ctx, e.Children(0), srcChk)
-		if err != nil {
-			return err
-		}
-		if srcChk.NumRows() == 0 {
-			break
-		}
-		e.rowChunks.Add(srcChk)
-	}
-	e.initPointers()
-	e.initCompareFuncs()
-	e.buildKeyColumns()
-	return nil
-}
-
-const topNCompactionFactor = 4
-
-func (e *TopNExec) executeTopN(ctx context.Context) error {
-	heap.Init(e.chkHeap)
-	for uint64(len(e.rowPtrs)) > e.totalLimit {
-		// The number of rows we loaded may exceeds total limit, remove greatest rows by Pop.
-		heap.Pop(e.chkHeap)
-	}
-	childRowChk := exec.TryNewCacheChunk(e.Children(0))
-	for {
-		err := exec.Next(ctx, e.Children(0), childRowChk)
-		if err != nil {
-			return err
-		}
-		if childRowChk.NumRows() == 0 {
-			break
-		}
-		err = e.processChildChk(childRowChk)
-		if err != nil {
-			return err
-		}
-		if e.rowChunks.Len() > len(e.rowPtrs)*topNCompactionFactor {
-			err = e.doCompaction()
-			if err != nil {
-				return err
-			}
-		}
-	}
-	slices.SortFunc(e.rowPtrs, e.keyColumnsCompare)
-	return nil
-}
-
-func (e *TopNExec) processChildChk(childRowChk *chunk.Chunk) error {
-	for i := 0; i < childRowChk.NumRows(); i++ {
-		heapMaxPtr := e.rowPtrs[0]
-		var heapMax, next chunk.Row
-		heapMax = e.rowChunks.GetRow(heapMaxPtr)
-		next = childRowChk.GetRow(i)
-		if e.chkHeap.greaterRow(heapMax, next) {
-			// Evict heap max, keep the next row.
-			e.rowPtrs[0] = e.rowChunks.AppendRow(childRowChk.GetRow(i))
-			heap.Fix(e.chkHeap, 0)
-		}
-	}
-	return nil
-}
-
-// doCompaction rebuild the chunks and row pointers to release memory.
-// If we don't do compaction, in a extreme case like the child data is already ascending sorted
-// but we want descending top N, then we will keep all data in memory.
-// But if data is distributed randomly, this function will be called log(n) times.
-func (e *TopNExec) doCompaction() error {
-	newRowChunks := chunk.NewList(exec.RetTypes(e), e.InitCap(), e.MaxChunkSize())
-	newRowPtrs := make([]chunk.RowPtr, 0, e.rowChunks.Len())
-	for _, rowPtr := range e.rowPtrs {
-		newRowPtr := newRowChunks.AppendRow(e.rowChunks.GetRow(rowPtr))
-		newRowPtrs = append(newRowPtrs, newRowPtr)
-	}
-	newRowChunks.GetMemTracker().SetLabel(memory.LabelForRowChunks)
-	e.memTracker.ReplaceChild(e.rowChunks.GetMemTracker(), newRowChunks.GetMemTracker())
-	e.rowChunks = newRowChunks
-
-	e.memTracker.Consume(int64(-8 * len(e.rowPtrs)))
-	e.memTracker.Consume(int64(8 * len(newRowPtrs)))
-	e.rowPtrs = newRowPtrs
-	return nil
-}
diff --git a/pkg/executor/sortexec/topn.go b/pkg/executor/sortexec/topn.go
new file mode 100644
index 0000000000..a52de50c42
--- /dev/null
+++ b/pkg/executor/sortexec/topn.go
@@ -0,0 +1,246 @@
+// Copyright 2023 PingCAP, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sortexec
+
+import (
+	"container/heap"
+	"context"
+	"slices"
+
+	"github.com/pingcap/tidb/pkg/executor/internal/exec"
+	plannercore "github.com/pingcap/tidb/pkg/planner/core"
+	"github.com/pingcap/tidb/pkg/util/chunk"
+	"github.com/pingcap/tidb/pkg/util/memory"
+)
+
+// TopNExec implements a Top-N algorithm and it is built from a SELECT statement with ORDER BY and LIMIT.
+// Instead of sorting all the rows fetched from the table, it keeps the Top-N elements only in a heap to reduce memory usage.
+type TopNExec struct {
+	SortExec
+	Limit      *plannercore.PhysicalLimit
+	totalLimit uint64
+
+	// rowChunks is the chunks to store row values.
+	rowChunks *chunk.List
+	// rowPointer store the chunk index and row index for each row.
+	rowPtrs []chunk.RowPtr
+
+	chkHeap *topNChunkHeap
+}
+
+// topNChunkHeap implements heap.Interface.
+type topNChunkHeap struct {
+	*TopNExec
+}
+
+// Less implement heap.Interface, but since we mantains a max heap,
+// this function returns true if row i is greater than row j.
+func (h *topNChunkHeap) Less(i, j int) bool {
+	rowI := h.rowChunks.GetRow(h.rowPtrs[i])
+	rowJ := h.rowChunks.GetRow(h.rowPtrs[j])
+	return h.greaterRow(rowI, rowJ)
+}
+
+func (h *topNChunkHeap) greaterRow(rowI, rowJ chunk.Row) bool {
+	for i, colIdx := range h.keyColumns {
+		cmpFunc := h.keyCmpFuncs[i]
+		cmp := cmpFunc(rowI, colIdx, rowJ, colIdx)
+		if h.ByItems[i].Desc {
+			cmp = -cmp
+		}
+		if cmp > 0 {
+			return true
+		} else if cmp < 0 {
+			return false
+		}
+	}
+	return false
+}
+
+func (h *topNChunkHeap) Len() int {
+	return len(h.rowPtrs)
+}
+
+func (*topNChunkHeap) Push(interface{}) {
+	// Should never be called.
+}
+
+func (h *topNChunkHeap) Pop() interface{} {
+	h.rowPtrs = h.rowPtrs[:len(h.rowPtrs)-1]
+	// We don't need the popped value, return nil to avoid memory allocation.
+	return nil
+}
+
+func (h *topNChunkHeap) Swap(i, j int) {
+	h.rowPtrs[i], h.rowPtrs[j] = h.rowPtrs[j], h.rowPtrs[i]
+}
+
+// keyColumnsLess is the less function for key columns.
+func (e *TopNExec) keyColumnsLess(i, j chunk.RowPtr) bool {
+	rowI := e.rowChunks.GetRow(i)
+	rowJ := e.rowChunks.GetRow(j)
+	return e.lessRow(rowI, rowJ)
+}
+
+func (e *TopNExec) keyColumnsCompare(i, j chunk.RowPtr) int {
+	rowI := e.rowChunks.GetRow(i)
+	rowJ := e.rowChunks.GetRow(j)
+	return e.compressRow(rowI, rowJ)
+}
+
+func (e *TopNExec) initPointers() {
+	e.rowPtrs = make([]chunk.RowPtr, 0, e.rowChunks.Len())
+	e.memTracker.Consume(int64(8 * e.rowChunks.Len()))
+	for chkIdx := 0; chkIdx < e.rowChunks.NumChunks(); chkIdx++ {
+		rowChk := e.rowChunks.GetChunk(chkIdx)
+		for rowIdx := 0; rowIdx < rowChk.NumRows(); rowIdx++ {
+			e.rowPtrs = append(e.rowPtrs, chunk.RowPtr{ChkIdx: uint32(chkIdx), RowIdx: uint32(rowIdx)})
+		}
+	}
+}
+
+// Open implements the Executor Open interface.
+func (e *TopNExec) Open(ctx context.Context) error {
+	e.memTracker = memory.NewTracker(e.ID(), -1)
+	e.memTracker.AttachTo(e.Ctx().GetSessionVars().StmtCtx.MemTracker)
+
+	e.fetched = false
+	e.Idx = 0
+
+	return exec.Open(ctx, e.Children(0))
+}
+
+// Next implements the Executor Next interface.
+func (e *TopNExec) Next(ctx context.Context, req *chunk.Chunk) error {
+	req.Reset()
+	if !e.fetched {
+		e.totalLimit = e.Limit.Offset + e.Limit.Count
+		e.Idx = int(e.Limit.Offset)
+		err := e.loadChunksUntilTotalLimit(ctx)
+		if err != nil {
+			return err
+		}
+		err = e.executeTopN(ctx)
+		if err != nil {
+			return err
+		}
+		e.fetched = true
+	}
+	if e.Idx >= len(e.rowPtrs) {
+		return nil
+	}
+	if !req.IsFull() {
+		numToAppend := min(len(e.rowPtrs)-e.Idx, req.RequiredRows()-req.NumRows())
+		rows := make([]chunk.Row, numToAppend)
+		for index := 0; index < numToAppend; index++ {
+			rows[index] = e.rowChunks.GetRow(e.rowPtrs[e.Idx])
+			e.Idx++
+		}
+		req.AppendRows(rows)
+	}
+	return nil
+}
+
+func (e *TopNExec) loadChunksUntilTotalLimit(ctx context.Context) error {
+	e.chkHeap = &topNChunkHeap{e}
+	e.rowChunks = chunk.NewList(exec.RetTypes(e), e.InitCap(), e.MaxChunkSize())
+	e.rowChunks.GetMemTracker().AttachTo(e.memTracker)
+	e.rowChunks.GetMemTracker().SetLabel(memory.LabelForRowChunks)
+	for uint64(e.rowChunks.Len()) < e.totalLimit {
+		srcChk := exec.TryNewCacheChunk(e.Children(0))
+		// adjust required rows by total limit
+		srcChk.SetRequiredRows(int(e.totalLimit-uint64(e.rowChunks.Len())), e.MaxChunkSize())
+		err := exec.Next(ctx, e.Children(0), srcChk)
+		if err != nil {
+			return err
+		}
+		if srcChk.NumRows() == 0 {
+			break
+		}
+		e.rowChunks.Add(srcChk)
+	}
+	e.initPointers()
+	e.initCompareFuncs()
+	e.buildKeyColumns()
+	return nil
+}
+
+const topNCompactionFactor = 4
+
+func (e *TopNExec) executeTopN(ctx context.Context) error {
+	heap.Init(e.chkHeap)
+	for uint64(len(e.rowPtrs)) > e.totalLimit {
+		// The number of rows we loaded may exceeds total limit, remove greatest rows by Pop.
+		heap.Pop(e.chkHeap)
+	}
+	childRowChk := exec.TryNewCacheChunk(e.Children(0))
+	for {
+		err := exec.Next(ctx, e.Children(0), childRowChk)
+		if err != nil {
+			return err
+		}
+		if childRowChk.NumRows() == 0 {
+			break
+		}
+		err = e.processChildChk(childRowChk)
+		if err != nil {
+			return err
+		}
+		if e.rowChunks.Len() > len(e.rowPtrs)*topNCompactionFactor {
+			err = e.doCompaction()
+			if err != nil {
+				return err
+			}
+		}
+	}
+	slices.SortFunc(e.rowPtrs, e.keyColumnsCompare)
+	return nil
+}
+
+func (e *TopNExec) processChildChk(childRowChk *chunk.Chunk) error {
+	for i := 0; i < childRowChk.NumRows(); i++ {
+		heapMaxPtr := e.rowPtrs[0]
+		var heapMax, next chunk.Row
+		heapMax = e.rowChunks.GetRow(heapMaxPtr)
+		next = childRowChk.GetRow(i)
+		if e.chkHeap.greaterRow(heapMax, next) {
+			// Evict heap max, keep the next row.
+			e.rowPtrs[0] = e.rowChunks.AppendRow(childRowChk.GetRow(i))
+			heap.Fix(e.chkHeap, 0)
+		}
+	}
+	return nil
+}
+
+// doCompaction rebuild the chunks and row pointers to release memory.
+// If we don't do compaction, in a extreme case like the child data is already ascending sorted
+// but we want descending top N, then we will keep all data in memory.
+// But if data is distributed randomly, this function will be called log(n) times.
+func (e *TopNExec) doCompaction() error {
+	newRowChunks := chunk.NewList(exec.RetTypes(e), e.InitCap(), e.MaxChunkSize())
+	newRowPtrs := make([]chunk.RowPtr, 0, e.rowChunks.Len())
+	for _, rowPtr := range e.rowPtrs {
+		newRowPtr := newRowChunks.AppendRow(e.rowChunks.GetRow(rowPtr))
+		newRowPtrs = append(newRowPtrs, newRowPtr)
+	}
+	newRowChunks.GetMemTracker().SetLabel(memory.LabelForRowChunks)
+	e.memTracker.ReplaceChild(e.rowChunks.GetMemTracker(), newRowChunks.GetMemTracker())
+	e.rowChunks = newRowChunks
+
+	e.memTracker.Consume(int64(-8 * len(e.rowPtrs)))
+	e.memTracker.Consume(int64(8 * len(newRowPtrs)))
+	e.rowPtrs = newRowPtrs
+	return nil
+}
diff --git a/pkg/param/binary_params.go b/pkg/param/binary_params.go
index ce016f5016..602f41e9aa 100644
--- a/pkg/param/binary_params.go
+++ b/pkg/param/binary_params.go
@@ -138,6 +138,7 @@ func ExecArgs(typectx types.Context, binaryParams []BinaryParam) (params []expre
 			continue
 
 		case mysql.TypeDuration:
+			fsp := 0
 			switch len(binaryParams[i].Val) {
 			case 0:
 				tmp = "0"
@@ -155,13 +156,14 @@ func ExecArgs(typectx types.Context, binaryParams []BinaryParam) (params []expre
 					return
 				}
 				_, tmp = binaryDurationWithMS(1, binaryParams[i].Val, isNegative)
+				fsp = types.MaxFsp
 			default:
 				err = mysql.ErrMalformPacket
 				return
 			}
 			// TODO: generate the duration datum directly
 			var dur types.Duration
-			dur, _, err = types.ParseDuration(typectx, tmp.(string), types.MaxFsp)
+			dur, _, err = types.ParseDuration(typectx, tmp.(string), fsp)
 			err = typectx.HandleTruncate(err)
 			if err != nil {
 				return
diff --git a/pkg/sessionctx/stmtctx/stmtctx.go b/pkg/sessionctx/stmtctx/stmtctx.go
index 2216d05ef3..d7fa9eed2f 100644
--- a/pkg/sessionctx/stmtctx/stmtctx.go
+++ b/pkg/sessionctx/stmtctx/stmtctx.go
@@ -431,7 +431,7 @@ func NewStmtCtx() *StatementContext {
 
 // NewStmtCtxWithTimeZone creates a new StatementContext with the given timezone
 func NewStmtCtxWithTimeZone(tz *time.Location) *StatementContext {
-	intest.Assert(tz)
+	intest.AssertNotNil(tz)
 	sc := &StatementContext{}
 	sc.typeCtx = types.NewContext(types.DefaultStmtFlags, tz, sc.AppendWarning)
 	return sc
@@ -451,7 +451,7 @@ func (sc *StatementContext) TimeZone() *time.Location {
 
 // SetTimeZone sets the timezone
 func (sc *StatementContext) SetTimeZone(tz *time.Location) {
-	intest.Assert(tz)
+	intest.AssertNotNil(tz)
 	sc.typeCtx = sc.typeCtx.WithLocation(tz)
 }
 
diff --git a/pkg/types/context.go b/pkg/types/context.go
index b969e4801f..af678ebc67 100644
--- a/pkg/types/context.go
+++ b/pkg/types/context.go
@@ -211,7 +211,7 @@ func (c *Context) WithFlags(f Flags) Context {
 
 // WithLocation returns a new context with the given location
 func (c *Context) WithLocation(loc *time.Location) Context {
-	intest.Assert(loc)
+	intest.AssertNotNil(loc)
 	ctx := *c
 	ctx.loc = loc
 	return ctx
@@ -219,7 +219,7 @@ func (c *Context) WithLocation(loc *time.Location) Context {
 
 // Location returns the location of the context
 func (c *Context) Location() *time.Location {
-	intest.Assert(c.loc)
+	intest.AssertNotNil(c.loc)
 	if c.loc == nil {
 		// c.loc should always not be nil, just make the code safe here.
 		return time.UTC


Test patch:
diff --git a/pkg/executor/internal/calibrateresource/calibrate_resource_test.go b/pkg/executor/internal/calibrateresource/calibrate_resource_test.go
index af3a017fcb..681ced04ff 100644
--- a/pkg/executor/internal/calibrateresource/calibrate_resource_test.go
+++ b/pkg/executor/internal/calibrateresource/calibrate_resource_test.go
@@ -15,6 +15,7 @@
 package calibrateresource_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"testing"
@@ -22,6 +23,7 @@ import (
 
 	"github.com/pingcap/errors"
 	"github.com/pingcap/failpoint"
+	"github.com/pingcap/kvproto/pkg/meta_storagepb"
 	"github.com/pingcap/tidb/pkg/domain"
 	"github.com/pingcap/tidb/pkg/parser/mysql"
 	"github.com/pingcap/tidb/pkg/testkit"
@@ -783,13 +785,18 @@ type mockResourceGroupProvider struct {
 	cfg rmclient.Config
 }
 
-func (p *mockResourceGroupProvider) LoadGlobalConfig(ctx context.Context, names []string, configPath string) ([]pd.GlobalConfigItem, int64, error) {
-	if configPath != "resource_group/controller" {
-		return nil, 0, errors.New("unsupported configPath")
+func (p *mockResourceGroupProvider) Get(ctx context.Context, key []byte, opts ...pd.OpOption) (*meta_storagepb.GetResponse, error) {
+	if !bytes.Equal(pd.ControllerConfigPathPrefixBytes, key) {
+		return nil, errors.New("unsupported configPath")
 	}
 	payload, _ := json.Marshal(&p.cfg)
-	item := pd.GlobalConfigItem{
-		PayLoad: payload,
-	}
-	return []pd.GlobalConfigItem{item}, 0, nil
+	return &meta_storagepb.GetResponse{
+		Count: 1,
+		Kvs: []*meta_storagepb.KeyValue{
+			{
+				Key:   key,
+				Value: payload,
+			},
+		},
+	}, nil
 }
diff --git a/pkg/server/conn_stmt_params_test.go b/pkg/server/conn_stmt_params_test.go
index beaa275692..61dd33ffcc 100644
--- a/pkg/server/conn_stmt_params_test.go
+++ b/pkg/server/conn_stmt_params_test.go
@@ -214,7 +214,7 @@ func TestParseExecArgs(t *testing.T) {
 			},
 			nil,
 			types.ErrTruncatedWrongVal,
-			types.Duration{Duration: types.MinTime, Fsp: types.MaxFsp},
+			types.Duration{Duration: types.MinTime, Fsp: 0},
 		},
 		{
 			args{
@@ -226,7 +226,7 @@ func TestParseExecArgs(t *testing.T) {
 			},
 			nil,
 			nil,
-			types.Duration{Duration: time.Duration(0), Fsp: types.MaxFsp},
+			types.Duration{Duration: time.Duration(0), Fsp: 0},
 		},
 		// For error test
 		{
diff --git a/pkg/util/intest/BUILD.bazel b/pkg/util/intest/BUILD.bazel
index bab02008d0..d34d4b1cbd 100644
--- a/pkg/util/intest/BUILD.bazel
+++ b/pkg/util/intest/BUILD.bazel
@@ -18,6 +18,7 @@ go_test(
     flaky = True,
     deps = [
         ":intest",
+        "@com_github_pingcap_errors//:errors",
         "@com_github_stretchr_testify//require",
     ],
 )
diff --git a/pkg/util/intest/assert.go b/pkg/util/intest/assert.go
index c40d6cc71a..1d96c38ff1 100644
--- a/pkg/util/intest/assert.go
+++ b/pkg/util/intest/assert.go
@@ -21,63 +21,66 @@ import (
 	"reflect"
 )
 
-// Assert asserts a condition. It only works in test (intest.InTest == true).
-// You can assert a condition like this to assert a variable `foo` is not nil: `assert.Assert(foo != nil)`.
-// Or you can pass foo as a parameter directly for simple: `assert.Assert(foo)`
-// You can also assert a function that returns a bool: `intest.Assert(func() bool { return foo != nil })`
-// If you pass a function without a signature `func() bool`, the function will always panic.
-func Assert(cond any, msgAndArgs ...any) {
+// Assert asserts a condition is true
+func Assert(cond bool, msgAndArgs ...any) {
+	if InTest && !cond {
+		doPanic("", msgAndArgs...)
+	}
+}
+
+// AssertNoError asserts an error is nil
+func AssertNoError(err error, msgAndArgs ...any) {
+	if InTest && err != nil {
+		doPanic(fmt.Sprintf("error is not nil: %+v", err), msgAndArgs...)
+	}
+}
+
+// AssertNotNil asserts an object is not nil
+func AssertNotNil(obj any, msgAndArgs ...any) {
 	if InTest {
-		assert(cond, msgAndArgs...)
+		Assert(obj != nil, msgAndArgs...)
+		value := reflect.ValueOf(obj)
+		switch value.Kind() {
+		case reflect.Func, reflect.Chan, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
+			Assert(!value.IsNil(), msgAndArgs...)
+		}
 	}
 }
 
 // AssertFunc asserts a function condition
 func AssertFunc(fn func() bool, msgAndArgs ...any) {
 	if InTest {
-		assert(fn(), msgAndArgs...)
+		Assert(fn != nil, msgAndArgs...)
+		Assert(fn(), msgAndArgs...)
 	}
 }
 
-func assert(cond any, msgAndArgs ...any) {
-	if !checkAssertObject(cond) {
-		doPanic(msgAndArgs...)
-	}
+func doPanic(extraMsg string, userMsgAndArgs ...any) {
+	panic(assertionFailedMsg(extraMsg, userMsgAndArgs...))
 }
 
-func doPanic(msgAndArgs ...any) {
-	panic(assertionFailedMsg(msgAndArgs...))
-}
+func assertionFailedMsg(extraMsg string, userMsgAndArgs ...any) string {
+	msg := "assert failed"
+	if len(userMsgAndArgs) == 0 {
+		if extraMsg != "" {
+			msg = fmt.Sprintf("%s, %s", msg, extraMsg)
+		}
+		return msg
+	}
 
-func assertionFailedMsg(msgAndArgs ...any) string {
-	if len(msgAndArgs) == 0 {
-		return "assert failed"
+	if len(userMsgAndArgs) == 0 {
+		return fmt.Sprintf("assert failed, %s", extraMsg)
 	}
 
-	msg, ok := msgAndArgs[0].(string)
+	userMsg, ok := userMsgAndArgs[0].(string)
 	if !ok {
-		msg = fmt.Sprintf("%+v", msgAndArgs[0])
+		userMsg = fmt.Sprintf("%+v", userMsgAndArgs[0])
 	}
 
-	msg = fmt.Sprintf("assert failed: %s", msg)
-	return fmt.Sprintf(msg, msgAndArgs[1:]...)
-}
-
-func checkAssertObject(obj any) bool {
-	if obj == nil {
-		return false
+	msg = fmt.Sprintf("%s, %s", msg, userMsg)
+	if extraMsg != "" {
+		msg = fmt.Sprintf("%s, %s", msg, extraMsg)
 	}
 
-	value := reflect.ValueOf(obj)
-	switch value.Kind() {
-	case reflect.Bool:
-		return obj.(bool)
-	case reflect.Func:
-		panic("you should use `intest.Assert(fn != nil)` to assert a function is not nil, " +
-			"or use `intest.AssertFunc(fn)` to assert a function's return value is true")
-	case reflect.Chan, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
-		return !value.IsNil()
-	default:
-		return true
-	}
+	return fmt.Sprintf(msg, userMsgAndArgs[1:]...)
 }
diff --git a/pkg/util/intest/assert_test.go b/pkg/util/intest/assert_test.go
index c85fc8d5b1..6a14cf7939 100644
--- a/pkg/util/intest/assert_test.go
+++ b/pkg/util/intest/assert_test.go
@@ -15,8 +15,10 @@
 package intest_test
 
 import (
+	"strings"
 	"testing"
 
+	"github.com/pingcap/errors"
 	"github.com/pingcap/tidb/pkg/util/intest"
 	"github.com/stretchr/testify/require"
 )
@@ -26,37 +28,55 @@ type foo struct{}
 func TestAssert(t *testing.T) {
 	require.True(t, intest.InTest)
 	checkAssert(t, true, true)
-	checkAssert(t, "", true)
-	checkAssert(t, "abc", true)
-	checkAssert(t, 0, true)
-	checkAssert(t, 123, true)
-	checkAssert(t, foo{}, true)
-	checkAssert(t, &foo{}, true)
 	checkAssert(t, false, false)
-	checkAssert(t, false, false, "assert failed: msg1", "msg1")
-	checkAssert(t, false, false, "assert failed: msg2 a b 1", "msg2 %s %s %d", "a", "b", 1)
-	checkAssert(t, false, false, "assert failed: 123", 123)
-	checkAssert(t, nil, false)
+	checkAssert(t, false, false, "assert failed, msg1", "msg1")
+	checkAssert(t, false, false, "assert failed, msg2 a b 1", "msg2 %s %s %d", "a", "b", 1)
+	checkAssert(t, false, false, "assert failed, 123", 123)
+	checkAssertNotNil(t, "", true)
+	checkAssertNotNil(t, "abc", true)
+	checkAssertNotNil(t, 0, true)
+	checkAssertNotNil(t, 123, true)
+	checkAssertNotNil(t, foo{}, true)
+	checkAssertNotNil(t, &foo{}, true)
+	checkAssertNotNil(t, nil, false)
+	checkAssertNotNil(t, true, true)
+	checkAssertNotNil(t, false, true)
 	var f *foo
-	checkAssert(t, f, false)
-	checkAssert(t, func() bool { return true }, false, "you should use `intest.Assert(fn != nil)` to assert a function is not nil, or use `intest.AssertFunc(fn)` to assert a function's return value is true")
-	checkAssert(t, func(_ string) bool { return true }, false, "you should use `intest.Assert(fn != nil)` to assert a function is not nil, or use `intest.AssertFunc(fn)` to assert a function's return value is true")
+	checkAssertNotNil(t, f, false)
+	checkAssertNotNil(t, func() bool { return true }, true)
+	checkAssertNotNil(t, func() bool { return false }, true)
+	checkAssertNotNil(t, func(_ string) bool { return true }, true)
+	checkAssertNotNil(t, nil, false, "assert failed, msg1", "msg1")
+	checkAssertNotNil(t, nil, false, "assert failed, msg2 a b 1", "msg2 %s %s %d", "a", "b", 1)
 	checkFuncAssert(t, func() bool { panic("inner panic1") }, false, "inner panic1")
 	checkFuncAssert(t, func() bool { return true }, true)
 	checkFuncAssert(t, func() bool { return false }, false)
-	checkFuncAssert(t, func() bool { return false }, false, "assert failed: msg3", "msg3")
-	checkFuncAssert(t, func() bool { return false }, false, "assert failed: msg4 c d 2", "msg4 %s %s %d", "c", "d", 2)
+	checkFuncAssert(t, func() bool { return false }, false, "assert failed, msg3", "msg3")
+	checkFuncAssert(t, func() bool { return false }, false, "assert failed, msg4 c d 2", "msg4 %s %s %d", "c", "d", 2)
 	checkFuncAssert(t, func() bool { panic("inner panic2") }, false, "inner panic2")
+	checkFuncAssert(t, nil, false)
+	checkAssertNoError(t, nil, true)
+	checkAssertNoError(t, errors.New("mock err1"), false, "assert failed, error is not nil: mock err1")
+	var err error
+	checkAssertNoError(t, err, true)
 }
 
 func checkFuncAssert(t *testing.T, fn func() bool, pass bool, msgAndArgs ...any) {
 	doCheckAssert(t, intest.AssertFunc, fn, pass, msgAndArgs...)
 }
 
-func checkAssert(t *testing.T, cond any, pass bool, msgAndArgs ...any) {
+func checkAssert(t *testing.T, cond bool, pass bool, msgAndArgs ...any) {
 	doCheckAssert(t, intest.Assert, cond, pass, msgAndArgs...)
 }
 
+func checkAssertNotNil(t *testing.T, obj any, pass bool, msgAndArgs ...any) {
+	doCheckAssert(t, intest.AssertNotNil, obj, pass, msgAndArgs...)
+}
+
+func checkAssertNoError(t *testing.T, err error, pass bool, msgAndArgs ...any) {
+	doCheckAssert(t, intest.AssertNoError, err, pass, msgAndArgs...)
+}
+
 func doCheckAssert(t *testing.T, fn any, cond any, pass bool, msgAndArgs ...any) {
 	expectMsg := "assert failed"
 	if len(msgAndArgs) > 0 {
@@ -64,32 +84,39 @@ func doCheckAssert(t *testing.T, fn any, cond any, pass bool, msgAndArgs ...any)
 		msgAndArgs = msgAndArgs[1:]
 	}
 
+	fail := ""
+	onlyCheckPrefix := false
 	if !pass {
 		defer func() {
+			if fail != "" {
+				require.FailNow(t, fail)
+			}
 			r := recover()
 			require.NotNil(t, r)
-			require.Equal(t, expectMsg, r)
+			if onlyCheckPrefix {
+				require.True(t, strings.HasPrefix(r.(string), expectMsg), "%q\nshould have prefix \n%q", r, expectMsg)
+				require.Contains(t, r.(string), expectMsg)
+			} else {
+				require.Equal(t, expectMsg, r)
+			}
 		}()
 	}
 
-	testFn, ok := fn.(func(any, ...any))
-	if !ok {
-		if fnAssert, ok := fn.(func(func() bool, ...any)); ok {
-			testFn = func(any, ...any) {
-				fnAssert(cond.(func() bool), msgAndArgs...)
-			}
+	switch assertFn := fn.(type) {
+	case func(bool, ...any):
+		assertFn(cond.(bool), msgAndArgs...)
+	case func(any, ...any):
+		assertFn(cond, msgAndArgs...)
+	case func(func() bool, ...any):
+		assertFn(cond.(func() bool), msgAndArgs...)
+	case func(error, ...any):
+		if cond == nil {
+			assertFn(nil, msgAndArgs...)
 		} else {
-			require.FailNow(t, "invalid assert function")
+			onlyCheckPrefix = true
+			assertFn(cond.(error), msgAndArgs...)
 		}
+	default:
+		fail = "invalid input assert function"
 	}
-
-	if len(msgAndArgs) == 0 {
-		testFn(cond)
-	}
-
-	if len(msgAndArgs) == 1 {
-		testFn(cond, msgAndArgs[0])
-	}
-
-	testFn(cond, msgAndArgs...)
 }
diff --git a/pkg/util/intest/common.go b/pkg/util/intest/common.go
index 56b928bc9e..a2ebccd4b3 100644
--- a/pkg/util/intest/common.go
+++ b/pkg/util/intest/common.go
@@ -21,7 +21,15 @@ const InTest = false
 
 // Assert is a stub function in release build.
 // See the same function in `util/intest/assert.go` for the real implement in test.
-func Assert(_ any, _ ...any) {}
+func Assert(_ bool, _ ...any) {}
+
+// AssertNotNil is a stub function in release build.
+// See the same function in `util/intest/assert.go` for the real implement in test.
+func AssertNotNil(_ any, _ ...any) {}
+
+// AssertNoError is a stub function in release build.
+// See the same function in `util/intest/assert.go` for the real implement in test.
+func AssertNoError(_ error, _ ...any) {}
 
 // AssertFunc is a stub function in release build.
 // See the same function `util/intest/assert.go` for the real implement in test.