Repository: pingcap/tidb
Pull request #60685: server: check connection is available in SQLKiller
Author: login: Defined2014
Labels: name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
name: release-note-none
description: Denotes a PR that doesn't merit a release note.
name: approved
name: lgtm
Created: 2025-04-21T08:39:29Z | Merged: 2025-05-07T03:44:48Z

<!--

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: close #57531

Problem Summary:

### What changed and how does it work?

### Check List

Tests <!-- At least one of them must be included. -->

- [x] Unit test
- [ ] Integration test
- [x] Manual test (add detailed scripts or steps below)
As #57531 metioned, run `select sleep(300)` and kill the mysql-client. Then the sql can't found by `show processlist`.
- [ ] No need to test
  > - [ ] I checked and no code files have been changed.
  > <!-- Or your custom  "No need to test" reasons -->

Side effects

- [ ] Performance regression: Consumes more CPU
- [ ] Performance regression: Consumes more Memory
- [ ] Breaking backward compatibility

Documentation

- [ ] Affects user behaviors
- [ ] Contains syntax changes
- [ ] Contains variable changes
- [ ] Contains experimental features
- [ ] Changes MySQL compatibility

### Release note

<!-- compatibility change, improvement, bugfix, and new feature need a release note -->

Please refer to [Release Notes Language Style Guide](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/release-notes-style-guide.html) to write a quality release note.

```release-note
None
```


Linked issue #57531: TiDB can't close connection immediately after killing client process
## Bug Report

Please answer these questions before submitting your issue. Thanks!

### 1. Minimal reproduce step (Required)
Run a tidb instance locally, and execute `select sleep(300);` in a connection.
Then kill this client process `ps aux | grep -i mysql | grep -v grep | awk '{print $2}' | xargs kill`


### 2. What did you expect to see? (Required)

The connection is closed immediately like mysql.

### 3. What did you see instead (Required)

The connection is still in processlist and closed after 300s of execution.

### 4. What is your TiDB version? (Required)

v8.2.0

Linked issue comments: user: login: pcqz
created_at: 2024-11-20T04:04:51Z
body: /report customer
author_association: NONE
user: login: yibin87
created_at: 2024-11-25T08:16:52Z
body: The sleep command can be killed by "kill" command. It seems the killing of client doesn't send kill signal to the target sleep process. I think this should be a sql infra issue.
author_association: CONTRIBUTOR
user: login: yibin87
created_at: 2024-11-25T08:17:06Z
body: /sig infra
author_association: CONTRIBUTOR
user: login: ti-chi-bot[bot]
created_at: 2024-11-25T08:17:10Z
body: @yibin87: The label(s) `sig/infra` cannot be applied, because the repository doesn't have them.

<details>

In response to [this](https://github.com/pingcap/tidb/issues/57531#issuecomment-2497196252):

>/sig infra


Instructions for interacting with me using PR comments are available [here](https://prow.tidb.net/command-help).  If you have questions or suggestions related to my behavior, please file an issue against the [ti-community-infra/tichi](https://github.com/ti-community-infra/tichi/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: yibin87
created_at: 2024-11-25T08:17:23Z
body: /sig sql-infra
author_association: CONTRIBUTOR
user: login: yibin87
created_at: 2024-11-25T08:17:38Z
body: /remove-sig execution
author_association: CONTRIBUTOR
user: login: Defined2014
created_at: 2024-11-28T07:04:02Z
body: The biggest different is MySQL could sense the tcp connect closed which TiDB can't.

MySQL, it replied a response after the kill processed:
![Image](https://github.com/user-attachments/assets/389b9b11-0420-4f54-a3d0-674838912a46)

If we want do it, we can monitor the fd of conn in the background and terminate SQL when a read signal is received and EOF is read.

A very hack way to acheive it

```diff
diff --git a/pkg/server/conn.go b/pkg/server/conn.go
index e201ff3cb4..05c6e5085a 100644
--- a/pkg/server/conn.go
+++ b/pkg/server/conn.go
@@ -1375,6 +1375,14 @@ func (cc *clientConn) dispatch(ctx context.Context, data []byte) error {
                        data = data[:len(data)-1]
                        dataStr = string(hack.String(data))
                }
+               go func(cc *clientConn) {
+                       b := make([]byte, 100)
+                       if idx := strings.Index(dataStr, "sleep"); idx >= 0 {
+                               if _, err := cc.bufReadConn.Conn.Read(b); err != nil {
+                                       killQuery(cc, false, false)
+                               }
+                       }
+               }(cc)
                return cc.handleQuery(ctx, dataStr)
        case mysql.ComFieldList:
                return cc.handleFieldList(ctx, dataStr)
```
author_association: CONTRIBUTOR
user: login: dveeden
created_at: 2024-12-20T07:11:12Z
body: This is what go-sql-driver/mysql uses: https://github.com/go-sql-driver/mysql/blob/v1.8.1/conncheck.go
author_association: CONTRIBUTOR

Diff:
diff --git a/pkg/server/conn.go b/pkg/server/conn.go
index 4afa7beb83f9b..78a261c9fa53a 100644
--- a/pkg/server/conn.go
+++ b/pkg/server/conn.go
@@ -2076,9 +2076,17 @@ func (cc *clientConn) handleStmt(
 				//nolint: errcheck
 				rs.Finish()
 			})
+		fn := func() bool {
+			if cc.bufReadConn != nil {
+				return cc.bufReadConn.IsAlive() != 0
+			}
+			return true
+		}
+		cc.ctx.GetSessionVars().SQLKiller.IsConnectionAlive.Store(&fn)
 		cc.ctx.GetSessionVars().SQLKiller.InWriteResultSet.Store(true)
 		defer cc.ctx.GetSessionVars().SQLKiller.InWriteResultSet.Store(false)
 		defer cc.ctx.GetSessionVars().SQLKiller.ClearFinishFunc()
+		defer cc.ctx.GetSessionVars().SQLKiller.IsConnectionAlive.Store(nil)
 		if retryable, err := cc.writeResultSet(ctx, rs, false, status, 0); err != nil {
 			return retryable, err
 		}
diff --git a/pkg/server/conn_stmt.go b/pkg/server/conn_stmt.go
index 052e1b2014fcc..652b4f56efc0a 100644
--- a/pkg/server/conn_stmt.go
+++ b/pkg/server/conn_stmt.go
@@ -231,6 +231,16 @@ func (cc *clientConn) executePlanCacheStmt(ctx context.Context, stmt any, args [
 	ctx = context.WithValue(ctx, execdetails.StmtExecDetailKey, &execdetails.StmtExecDetails{})
 	ctx = context.WithValue(ctx, util.ExecDetailsKey, &util.ExecDetails{})
 	ctx = context.WithValue(ctx, util.RUDetailsCtxKey, util.NewRUDetails())
+
+	fn := func() bool {
+		if cc.bufReadConn != nil {
+			return cc.bufReadConn.IsAlive() != 0
+		}
+		return true
+	}
+	cc.ctx.GetSessionVars().SQLKiller.IsConnectionAlive.Store(&fn)
+	defer cc.ctx.GetSessionVars().SQLKiller.IsConnectionAlive.Store(nil)
+
 	//nolint:forcetypeassert
 	retryable, err := cc.executePreparedStmtAndWriteResult(ctx, stmt.(PreparedStatement), args, useCursor)
 	if err != nil {
diff --git a/pkg/server/internal/util/buffered_read_conn.go b/pkg/server/internal/util/buffered_read_conn.go
index d6bc1f24c9bc4..e425bdb1f5b8c 100644
--- a/pkg/server/internal/util/buffered_read_conn.go
+++ b/pkg/server/internal/util/buffered_read_conn.go
@@ -16,7 +16,10 @@ package util
 
 import (
 	"bufio"
+	"io"
 	"net"
+	"sync"
+	"time"
 )
 
 // DefaultReaderSize is the default size of bufio.Reader.
@@ -26,11 +29,15 @@ const DefaultReaderSize = 16 * 1024
 type BufferedReadConn struct {
 	net.Conn
 	rb *bufio.Reader
+	// `mu` is for `IsAlive()` function.
+	// We use this to ensure that `SetReadDeadline` is not called concurrently.
+	mu *sync.Mutex
 }
 
 // NewBufferedReadConn creates a BufferedReadConn.
 func NewBufferedReadConn(conn net.Conn) *BufferedReadConn {
 	return &BufferedReadConn{
+		mu:   &sync.Mutex{},
 		Conn: conn,
 		rb:   bufio.NewReaderSize(conn, DefaultReaderSize),
 	}
@@ -40,3 +47,38 @@ func NewBufferedReadConn(conn net.Conn) *BufferedReadConn {
 func (conn BufferedReadConn) Read(b []byte) (n int, err error) {
 	return conn.rb.Read(b)
 }
+
+// Peek peeks from the connection.
+func (conn BufferedReadConn) Peek(n int) ([]byte, error) {
+	return conn.rb.Peek(n)
+}
+
+// IsAlive detects the connection is alive or not.
+// return value < 0, means unknow
+// return value = 0, means not alive
+// return value = 1, means still alive
+func (conn BufferedReadConn) IsAlive() int {
+	if conn.mu.TryLock() {
+		defer conn.mu.Unlock()
+		err := conn.SetReadDeadline(time.Now().Add(30 * time.Microsecond))
+		if err != nil {
+			return -1
+		}
+		// nolint:errcheck
+		defer conn.SetReadDeadline(time.Time{})
+		// At the TCP level, a successful `Peek` operation doesn't guarantee
+		// the connection remains active. However, in the MySQL protocol,
+		// clients shouldn't send new data while the server is processing SQL.
+		// Therefore, we can safely assume `Peek` won't intercept any data
+		// during this period. Even if `Peek` does capture data, it only means
+		// the liveness check might be inaccurate - this won't impact the
+		// actual connection state or its operations.
+		_, err = conn.Peek(1)
+		if err == io.EOF {
+			return 0
+		} else if ne, ok := err.(net.Error); ok && ne.Timeout() {
+			return 1
+		}
+	}
+	return -1
+}
diff --git a/pkg/server/tests/commontest/tidb_test.go b/pkg/server/tests/commontest/tidb_test.go
index 6c73d966ec783..51d0c6900ca7f 100644
--- a/pkg/server/tests/commontest/tidb_test.go
+++ b/pkg/server/tests/commontest/tidb_test.go
@@ -31,6 +31,7 @@ import (
 	"sync/atomic"
 	"testing"
 	"time"
+	"unsafe"
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/pingcap/errors"
@@ -3397,3 +3398,64 @@ func TestBatchGetTypeForRowExpr(t *testing.T) {
 		ts.CheckRows(t, rows, "a b\nc d")
 	})
 }
+
+func TestIssue57531(t *testing.T) {
+	ts := servertestkit.CreateTidbTestSuite(t)
+
+	var rsCnt int
+	for i := range 2 {
+		ts.RunTests(t, nil, func(dbt *testkit.DBTestKit) {
+			var conn *sql.Conn
+			var netConn net.Conn
+			conn, _ = dbt.GetDB().Conn(context.Background())
+
+			// get the TCP connection
+			conn.Raw(func(driverConn any) error {
+				v := reflect.ValueOf(driverConn)
+				if v.Kind() == reflect.Ptr {
+					v = v.Elem()
+				}
+				f := v.FieldByName("netConn")
+				if f.IsValid() && f.Type().Implements(reflect.TypeOf((*net.Conn)(nil)).Elem()) {
+					netConn = *(*net.Conn)(unsafe.Pointer(f.UnsafeAddr()))
+				}
+				return nil
+			})
+
+			// execute `select sleep(300)`
+			go func() {
+				if i == 0 {
+					conn.QueryContext(context.Background(), "select sleep(300)")
+				} else {
+					stmt, err := conn.PrepareContext(context.Background(), "select sleep(?)")
+					require.NoError(t, err)
+					stmt.Exec(300)
+				}
+			}()
+			time.Sleep(200 * time.Millisecond)
+
+			// have two sessions
+			rsCnt = 0
+			rs := dbt.MustQuery("show processlist")
+			for rs.Next() {
+				rsCnt++
+			}
+			require.Equal(t, rsCnt, 2)
+
+			// close tcp connection
+			netConn.Close()
+		})
+
+		time.Sleep(10 * time.Millisecond)
+
+		// the `select sleep(300)` is killed
+		ts.RunTests(t, nil, func(dbt *testkit.DBTestKit) {
+			rsCnt = 0
+			rs := dbt.MustQuery("show processlist")
+			for rs.Next() {
+				rsCnt++
+			}
+			require.Equal(t, rsCnt, 1)
+		})
+	}
+}
diff --git a/pkg/util/deeptest/statictesthelper.go b/pkg/util/deeptest/statictesthelper.go
index b9493444037ae..5b4252dbdb3e2 100644
--- a/pkg/util/deeptest/statictesthelper.go
+++ b/pkg/util/deeptest/statictesthelper.go
@@ -142,7 +142,7 @@ func (h *staticTestHelper) assertDeepClonedEqual(t require.TestingT, valA, valB
 		for i := range valA.NumField() {
 			h.assertDeepClonedEqual(t, valA.Field(i), valB.Field(i), path+"."+valA.Type().Field(i).Name)
 		}
-	case reflect.Ptr:
+	case reflect.Ptr, reflect.UnsafePointer:
 		if valA.IsNil() && valB.IsNil() {
 			return
 		}
diff --git a/pkg/util/sqlkiller/BUILD.bazel b/pkg/util/sqlkiller/BUILD.bazel
index 5a4eacf70afd6..5a3a76d5a558e 100644
--- a/pkg/util/sqlkiller/BUILD.bazel
+++ b/pkg/util/sqlkiller/BUILD.bazel
@@ -7,6 +7,7 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//pkg/util/dbterror/exeerrors",
+        "//pkg/util/intest",
         "//pkg/util/logutil",
         "@com_github_pingcap_failpoint//:failpoint",
         "@org_uber_go_zap//:zap",
diff --git a/pkg/util/sqlkiller/sqlkiller.go b/pkg/util/sqlkiller/sqlkiller.go
index da81f99ee535d..c674a1c2f02f9 100644
--- a/pkg/util/sqlkiller/sqlkiller.go
+++ b/pkg/util/sqlkiller/sqlkiller.go
@@ -18,9 +18,11 @@ import (
 	"math/rand"
 	"sync"
 	"sync/atomic"
+	"time"
 
 	"github.com/pingcap/failpoint"
 	"github.com/pingcap/tidb/pkg/util/dbterror/exeerrors"
+	"github.com/pingcap/tidb/pkg/util/intest"
 	"github.com/pingcap/tidb/pkg/util/logutil"
 	"go.uber.org/zap"
 )
@@ -51,6 +53,9 @@ type SQLKiller struct {
 	// InWriteResultSet is used to indicate whether the query is currently calling clientConn.writeResultSet().
 	// If the query is in writeResultSet and Finish() can acquire rs.finishLock, we can assume the query is waiting for the client to receive data from the server over network I/O.
 	InWriteResultSet atomic.Bool
+
+	lastCheckTime     atomic.Pointer[time.Time]
+	IsConnectionAlive atomic.Pointer[func() bool]
 }
 
 // SendKillSignal sends a kill signal to the query.
@@ -122,6 +127,27 @@ func (killer *SQLKiller) HandleSignal() error {
 			}
 		}
 	})
+
+	// Checks if the connection is alive.
+	// For performance reasons, the check interval should be at least `checkConnectionAliveDur`(1 second).
+	fn := killer.IsConnectionAlive.Load()
+	lastCheckTime := killer.lastCheckTime.Load()
+	if fn != nil {
+		var checkConnectionAliveDur time.Duration = time.Second
+		now := time.Now()
+		if intest.InTest {
+			checkConnectionAliveDur = time.Millisecond
+		}
+		if lastCheckTime == nil {
+			killer.lastCheckTime.Store(&now)
+		} else if now.Sub(*lastCheckTime) > checkConnectionAliveDur {
+			killer.lastCheckTime.Store(&now)
+			if !(*fn)() {
+				atomic.CompareAndSwapUint32(&killer.Signal, 0, QueryInterrupted)
+			}
+		}
+	}
+
 	status := atomic.LoadUint32(&killer.Signal)
 	err := killer.getKillError(status)
 	if status == ServerMemoryExceeded {
@@ -137,4 +163,5 @@ func (killer *SQLKiller) Reset() {
 		logutil.BgLogger().Warn("kill finished", zap.Uint64("conn", killer.ConnID.Load()))
 	}
 	atomic.StoreUint32(&killer.Signal, 0)
+	killer.lastCheckTime.Store(nil)
 }


Reviews:
user: login: YangKeao
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T02:17:19Z
user: login: xhebox
body: LGTM otherwise
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:22:35Z
user: login: Defined2014
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:44:26Z
user: login: xhebox
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T03:07:40Z

Review comments:
diff_hunk: @@ -26,11 +29,13 @@ const DefaultReaderSize = 16 * 1024
 type BufferedReadConn struct {
 	net.Conn
 	rb *bufio.Reader
+	mu *sync.Mutex
path: pkg/server/internal/util/buffered_read_conn.go
user: login: xhebox
body: Maybe add some comments for this lock. It looks like a global lock while it is used for IsAlive only.
created_at: 2025-05-07T02:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +127,27 @@ func (killer *SQLKiller) HandleSignal() error {
 			}
 		}
 	})
+
+	// Checks if the connection is alive.
+	// For performance reasons, the check interval should be at least `checkConnectionAliveDur`(1 second).
+	fn := killer.IsConnectionAlive.Load()
+	lastCheckTime := killer.lastCheckTime.Load()
+	if fn != nil {
+		var checkConnectionAliveDur time.Duration = time.Second
+		now := time.Now()
+		if intest.InTest {
+			checkConnectionAliveDur = time.Millisecond
+		}
+		if lastCheckTime == nil {
+			killer.lastCheckTime.Store(&now)
+		} else if time.Since(*lastCheckTime) > checkConnectionAliveDur {
path: pkg/util/sqlkiller/sqlkiller.go
user: login: xhebox
body: Why not just `now.Sub(*lastCheckTime)`?
created_at: 2025-05-07T02:18:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,3 +45,38 @@ func NewBufferedReadConn(conn net.Conn) *BufferedReadConn {
 func (conn BufferedReadConn) Read(b []byte) (n int, err error) {
 	return conn.rb.Read(b)
 }
+
+// Peek peeks from the connection.
+func (conn BufferedReadConn) Peek(n int) ([]byte, error) {
+	return conn.rb.Peek(n)
+}
+
+// IsAlive detects the connection is alive or not.
+// return value < 0, means unknow
path: pkg/server/internal/util/buffered_read_conn.go
user: login: xhebox
body: I see that our judge is`IsAlive() != 0`, so we also kill queries when it is `unknow` state?
created_at: 2025-05-07T02:22:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,3 +45,38 @@ func NewBufferedReadConn(conn net.Conn) *BufferedReadConn {
 func (conn BufferedReadConn) Read(b []byte) (n int, err error) {
 	return conn.rb.Read(b)
 }
+
+// Peek peeks from the connection.
+func (conn BufferedReadConn) Peek(n int) ([]byte, error) {
+	return conn.rb.Peek(n)
+}
+
+// IsAlive detects the connection is alive or not.
+// return value < 0, means unknow
path: pkg/server/internal/util/buffered_read_conn.go
user: login: Defined2014
body: Kill it Only when `IsAlive() == 0`
created_at: 2025-05-07T02:25:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,11 +29,13 @@ const DefaultReaderSize = 16 * 1024
 type BufferedReadConn struct {
 	net.Conn
 	rb *bufio.Reader
+	mu *sync.Mutex
path: pkg/server/internal/util/buffered_read_conn.go
user: login: Defined2014
body: Done
created_at: 2025-05-07T02:43:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +127,27 @@ func (killer *SQLKiller) HandleSignal() error {
 			}
 		}
 	})
+
+	// Checks if the connection is alive.
+	// For performance reasons, the check interval should be at least `checkConnectionAliveDur`(1 second).
+	fn := killer.IsConnectionAlive.Load()
+	lastCheckTime := killer.lastCheckTime.Load()
+	if fn != nil {
+		var checkConnectionAliveDur time.Duration = time.Second
+		now := time.Now()
+		if intest.InTest {
+			checkConnectionAliveDur = time.Millisecond
+		}
+		if lastCheckTime == nil {
+			killer.lastCheckTime.Store(&now)
+		} else if time.Since(*lastCheckTime) > checkConnectionAliveDur {
path: pkg/util/sqlkiller/sqlkiller.go
user: login: Defined2014
body: Done
created_at: 2025-05-07T02:44:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-21T10:14:56Z
body: ## [Codecov](https://app.codecov.io/gh/pingcap/tidb/pull/60685?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) Report
Attention: Patch coverage is `94.82759%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 73.6238%. Comparing base [(`62d4284`)](https://app.codecov.io/gh/pingcap/tidb/commit/62d4284bee67e64f23b0a7b7f89488b26667c0eb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) to head [(`4f6c85b`)](https://app.codecov.io/gh/pingcap/tidb/commit/4f6c85b939718ea0eaace0943f850a3f2a85d398?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap).
> Report is 8 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff                @@
##             master     #60685        +/-   ##
================================================
+ Coverage   73.1298%   73.6238%   +0.4940%     
================================================
  Files          1722       1722                
  Lines        476908     479248      +2340     
================================================
+ Hits         348762     352841      +4079     
+ Misses       106738     104885      -1853     
- Partials      21408      21522       +114     
```

| [Flag](https://app.codecov.io/gh/pingcap/tidb/pull/60685/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | Coverage Δ | |
|---|---|---|
| [integration](https://app.codecov.io/gh/pingcap/tidb/pull/60685/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `42.5672% <59.6491%> (?)` | |
| [unit](https://app.codecov.io/gh/pingcap/tidb/pull/60685/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `72.3806% <94.8275%> (+0.0297%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Components](https://app.codecov.io/gh/pingcap/tidb/pull/60685/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | Coverage Δ | |
|---|---|---|
| [dumpling](https://app.codecov.io/gh/pingcap/tidb/pull/60685/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `52.7804% <ø> (ø)` | |
| [parser](https://app.codecov.io/gh/pingcap/tidb/pull/60685/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `∅ <ø> (∅)` | |
| [br](https://app.codecov.io/gh/pingcap/tidb/pull/60685/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `47.4433% <ø> (+0.0164%)` | :arrow_up: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: Defined2014
created_at: 2025-04-21T12:24:50Z
body: /retest
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-21T12:25:27Z
body: @Defined2014: Cannot trigger testing until a trusted user reviews the PR and leaves an `/ok-to-test` message.

<details>

In response to [this](https://github.com/pingcap/tidb/pull/60685#issuecomment-2818312409):

>/retest


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: Defined2014
created_at: 2025-04-23T12:29:04Z
body: /retest
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-23T12:29:27Z
body: @Defined2014: Cannot trigger testing until a trusted user reviews the PR and leaves an `/ok-to-test` message.

<details>

In response to [this](https://github.com/pingcap/tidb/pull/60685#issuecomment-2824125490):

>/retest


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: Defined2014
created_at: 2025-04-24T01:35:22Z
body: /retest
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-24T01:35:48Z
body: @Defined2014: Cannot trigger testing until a trusted user reviews the PR and leaves an `/ok-to-test` message.

<details>

In response to [this](https://github.com/pingcap/tidb/pull/60685#issuecomment-2825937824):

>/retest


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: Defined2014
created_at: 2025-05-06T08:07:51Z
body: /retest
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-05-06T08:08:13Z
body: @Defined2014: Cannot trigger testing until a trusted user reviews the PR and leaves an `/ok-to-test` message.

<details>

In response to [this](https://github.com/pingcap/tidb/pull/60685#issuecomment-2853643085):

>/retest


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-05-07T03:07:46Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/pingcap/tidb/pull/60685#pullrequestreview-2820201943" title="Approved">xhebox</a>*, *<a href="https://github.com/pingcap/tidb/pull/60685#pullrequestreview-2820145544" title="Approved">YangKeao</a>*

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

The pull request process is described [here](https://book.prow.tidb.net/#/workflows/pr)

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

- ~~[OWNERS](https://github.com/pingcap/tidb/blob/master/OWNERS)~~ [YangKeao,xhebox]

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":[]} -->
author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-05-07T03:07:50Z
body: [LGTM Timeline notifier]
---
Timeline:

- `2025-05-07 02:17:29.130053346 +0000 UTC m=+407896.312956592`: :ballot_box_with_check: agreed by [YangKeao](https://github.com/YangKeao).
- `2025-05-07 03:07:50.382192946 +0000 UTC m=+410917.565096205`: :ballot_box_with_check: agreed by [xhebox](https://github.com/xhebox).
author_association: NONE