Repository: tikv/client-go (go)

Issue:
handle `RegionError.UndeterminedResult` in client-go
`UndeterminedResult` is an new type error added to `RegionError`, see: https://github.com/pingcap/kvproto/pull/1310

When client get this error, it should return `ErrResultUndetermined` to the caller without retry.

The outside code in TiDB should process the `ErrResultUndetermined` because the txn is in an undetermined state currently. For example, TiDB should disconnect the current connection at this time.

Pull request:
txn: handle undetermined error in client go
close #1641

Patch:
diff --git a/config/retry/backoff.go b/config/retry/backoff.go
index 823ab12..586ae48 100644
--- a/config/retry/backoff.go
+++ b/config/retry/backoff.go
@@ -45,6 +45,7 @@ import (
 	"time"
 
 	"github.com/opentracing/opentracing-go"
+	"github.com/pingcap/kvproto/pkg/errorpb"
 	"github.com/pingcap/log"
 	"github.com/pkg/errors"
 	tikverr "github.com/tikv/client-go/v2/error"
@@ -413,3 +414,27 @@ func (b *Backoffer) CheckKilled() error {
 	}
 	return nil
 }
+
+// IsFakeRegionError returns true if err is fake region error.
+func IsFakeRegionError(err *errorpb.Error) bool {
+	return err != nil && err.GetEpochNotMatch() != nil && len(err.GetEpochNotMatch().CurrentRegions) == 0
+}
+
+// MayBackoffForRegionError do some backoff if needed.
+// If the `regionError` can retry but need to backoff, it backoff first and returns the result of the method `Backoff`.
+// Otherwise, it returns nil.
+func MayBackoffForRegionError(regionErr *errorpb.Error, bo *Backoffer) error {
+	if regionErr == nil {
+		return nil
+	}
+
+	if regionErr.GetEpochNotMatch() == nil || IsFakeRegionError(regionErr) {
+		// For other region error and the fake region error, backoff because
+		// there's something wrong.
+		return bo.Backoff(BoRegionMiss, errors.New(regionErr.String()))
+	}
+
+	// Here `regionErr.GetEpochNotMatch() != nil`
+	// For the real EpochNotMatch error, don't backoff and retry immediately.
+	return nil
+}
diff --git a/examples/gcworker/go.mod b/examples/gcworker/go.mod
index 7d8b42f..19986a7 100644
--- a/examples/gcworker/go.mod
+++ b/examples/gcworker/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/rawkv/go.mod b/examples/rawkv/go.mod
index 50e59ff..aaa9ad8 100644
--- a/examples/rawkv/go.mod
+++ b/examples/rawkv/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/txnkv/1pc_txn/go.mod b/examples/txnkv/1pc_txn/go.mod
index 887e01f..a27b17c 100644
--- a/examples/txnkv/1pc_txn/go.mod
+++ b/examples/txnkv/1pc_txn/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/txnkv/async_commit/go.mod b/examples/txnkv/async_commit/go.mod
index 2d547dd..e5ba2d2 100644
--- a/examples/txnkv/async_commit/go.mod
+++ b/examples/txnkv/async_commit/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/txnkv/delete_range/go.mod b/examples/txnkv/delete_range/go.mod
index bec9758..56acb30 100644
--- a/examples/txnkv/delete_range/go.mod
+++ b/examples/txnkv/delete_range/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/txnkv/go.mod b/examples/txnkv/go.mod
index 0a5232e..6f62e3b 100644
--- a/examples/txnkv/go.mod
+++ b/examples/txnkv/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/txnkv/pessimistic_txn/go.mod b/examples/txnkv/pessimistic_txn/go.mod
index b328bb3..1bacc57 100644
--- a/examples/txnkv/pessimistic_txn/go.mod
+++ b/examples/txnkv/pessimistic_txn/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/examples/txnkv/unsafedestoryrange/go.mod b/examples/txnkv/unsafedestoryrange/go.mod
index 08c0fae..2a4f8cc 100644
--- a/examples/txnkv/unsafedestoryrange/go.mod
+++ b/examples/txnkv/unsafedestoryrange/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122 // indirect
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc // indirect
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb // indirect
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.20.5 // indirect
diff --git a/go.mod b/go.mod
index fa13809..f489e48 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86
 	github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb
 	github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.20.5
diff --git a/go.sum b/go.sum
index e0348b9..2c6e9bf 100644
--- a/go.sum
+++ b/go.sum
@@ -79,8 +79,8 @@ github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXH
 github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4=
 github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989 h1:surzm05a8C9dN8dIUmo4Be2+pMRb6f55i+UIYrluu2E=
 github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw=
-github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc h1:rNjlA9TDyZcJl8JSGhpGIpDewLWhqihWGPv/ExSenlQ=
-github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc/go.mod h1:rXxWk2UnwfUhLXha1jxRWPADw9eMZGWEWCg92Tgmb/8=
+github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb h1:cTPTugbdIV2yklyOM95nqhMn7S5PvGgzKykOo/f8VtE=
+github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb/go.mod h1:rXxWk2UnwfUhLXha1jxRWPADw9eMZGWEWCg92Tgmb/8=
 github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3 h1:HR/ylkkLmGdSSDaD8IDP+SZrdhV1Kibl9KrHxJ9eciw=
 github.com/pingcap/log v1.1.1-0.20221110025148-ca232912c9f3/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/internal/locate/region_request.go b/internal/locate/region_request.go
index 2300cb6..b309813 100644
--- a/internal/locate/region_request.go
+++ b/internal/locate/region_request.go
@@ -725,11 +725,6 @@ func (s *RegionRequestSender) reset() {
 	s.failProxyStoreIDs = nil
 }
 
-// IsFakeRegionError returns true if err is fake region error.
-func IsFakeRegionError(err *errorpb.Error) bool {
-	return err != nil && err.GetEpochNotMatch() != nil && len(err.GetEpochNotMatch().CurrentRegions) == 0
-}
-
 const slowLogSendReqTime = 100 * time.Millisecond
 
 // sendReqState represents the state of sending request with retry, which allows us to construct a state and start to
@@ -1977,7 +1972,19 @@ func failpointSendReqResult(req *tikvrpc.Request, et tikvrpc.EndpointType) (
 				err = errors.WithStack(tikverr.ErrTiFlashServerTimeout)
 				return
 			}
+		case "UndeterminedResult":
+			if req.Type == tikvrpc.CmdPrewrite {
+				resp = &tikvrpc.Response{
+					Resp: &kvrpcpb.PrewriteResponse{RegionError: &errorpb.Error{
+						UndeterminedResult: &errorpb.UndeterminedResult{
+							Message: "undetermined result",
+						},
+					}},
+				}
+				return
+			}
 		}
+
 	}
 	return
 }
diff --git a/txnkv/transaction/2pc.go b/txnkv/transaction/2pc.go
index 03dbc2c..ddca446 100644
--- a/txnkv/transaction/2pc.go
+++ b/txnkv/transaction/2pc.go
@@ -1496,14 +1496,8 @@ func sendTxnHeartBeat(
 			return 0, false, err
 		}
 		if regionErr != nil {
-			// For other region error and the fake region error, backoff because
-			// there's something wrong.
-			// For the real EpochNotMatch error, don't backoff.
-			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-				if err != nil {
-					return 0, false, err
-				}
+			if err = retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+				return 0, false, err
 			}
 			continue
 		}
diff --git a/txnkv/transaction/commit.go b/txnkv/transaction/commit.go
index 74e87ae..6ef986b 100644
--- a/txnkv/transaction/commit.go
+++ b/txnkv/transaction/commit.go
@@ -133,14 +133,16 @@ func (action actionCommit) handleSingleBatch(c *twoPhaseCommitter, bo *retry.Bac
 			return err
 		}
 		if regionErr != nil {
-			// For other region error and the fake region error, backoff because
-			// there's something wrong.
-			// For the real EpochNotMatch error, don't backoff.
-			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-				if err != nil {
-					return err
-				}
+			if regionErr.GetUndeterminedResult() != nil && !c.isAsyncCommit() && batch.isPrimary {
+				// If the current transaction is not async, and commit fails with error `UndeterminedResult`,
+				// it means the transaction's commit state is unknown.
+				// We should return the error `ErrResultUndetermined` to the caller
+				// to do the further handling (.i.e disconnect the connection).
+				return errors.WithStack(tikverr.ErrResultUndetermined)
+			}
+
+			if err = retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+				return err
 			}
 			same, err := batch.relocate(bo, c.store.GetRegionCache())
 			if err != nil {
diff --git a/txnkv/transaction/pessimistic.go b/txnkv/transaction/pessimistic.go
index 7dee2a7..3a3e0bb 100644
--- a/txnkv/transaction/pessimistic.go
+++ b/txnkv/transaction/pessimistic.go
@@ -220,14 +220,8 @@ func (action actionPessimisticLock) handleSingleBatch(
 func (action actionPessimisticLock) handleRegionError(
 	c *twoPhaseCommitter, bo *retry.Backoffer, batch *batchMutations, regionErr *errorpb.Error,
 ) (finished bool, err error) {
-	// For other region error and the fake region error, backoff because
-	// there's something wrong.
-	// For the real EpochNotMatch error, don't backoff.
-	if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-		err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-		if err != nil {
-			return true, err
-		}
+	if err := retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+		return true, err
 	}
 	same, err := batch.relocate(bo, c.store.GetRegionCache())
 	if err != nil {
diff --git a/txnkv/transaction/pipelined_flush.go b/txnkv/transaction/pipelined_flush.go
index 7de4599..30f0ee7 100644
--- a/txnkv/transaction/pipelined_flush.go
+++ b/txnkv/transaction/pipelined_flush.go
@@ -155,14 +155,8 @@ func (action actionPipelinedFlush) handleSingleBatch(
 			return err
 		}
 		if regionErr != nil {
-			// For other region error and the fake region error, backoff because
-			// there's something wrong.
-			// For the real EpochNotMatch error, don't backoff.
-			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-				if err != nil {
-					return err
-				}
+			if err = retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+				return err
 			}
 			if regionErr.GetDiskFull() != nil {
 				storeIds := regionErr.GetDiskFull().GetStoreId()
diff --git a/txnkv/transaction/prewrite.go b/txnkv/transaction/prewrite.go
index 2143ba9..a3a41b6 100644
--- a/txnkv/transaction/prewrite.go
+++ b/txnkv/transaction/prewrite.go
@@ -416,14 +416,16 @@ func (handler *prewrite1BatchReqHandler) sendReqAndCheck() (retryable bool, err
 //     doActionOnMutations directly and return retryable false regardless of success or failure.
 //  2. Other region errors.
 func (handler *prewrite1BatchReqHandler) handleRegionErr(regionErr *errorpb.Error) (retryable bool, err error) {
-	// For other region error and the fake region error, backoff because
-	// there's something wrong.
-	// For the real EpochNotMatch error, don't backoff.
-	if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-		err := handler.bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-		if err != nil {
-			return false, err
-		}
+	if regionErr.GetUndeterminedResult() != nil && handler.committer.isAsyncCommit() {
+		// If the current transaction is async commit and prewrite fails for `UndeterminedResult`,
+		// It means the transaction's commit state is unknown.
+		// We should return the error `ErrResultUndetermined` to the caller
+		// to for further handling (.i.e disconnect the connection).
+		return false, errors.WithStack(tikverr.ErrResultUndetermined)
+	}
+
+	if err = retry.MayBackoffForRegionError(regionErr, handler.bo); err != nil {
+		return false, err
 	}
 	if regionErr.GetDiskFull() != nil {
 		storeIds := regionErr.GetDiskFull().GetStoreId()
diff --git a/txnkv/txnsnapshot/scan.go b/txnkv/txnsnapshot/scan.go
index 160f2a7..f99cc7c 100644
--- a/txnkv/txnsnapshot/scan.go
+++ b/txnkv/txnsnapshot/scan.go
@@ -274,14 +274,8 @@ func (s *Scanner) getData(bo *retry.Backoffer) error {
 		if regionErr != nil {
 			logutil.BgLogger().Debug("scanner getData failed",
 				zap.Stringer("regionErr", regionErr))
-			// For other region error and the fake region error, backoff because
-			// there's something wrong.
-			// For the real EpochNotMatch error, don't backoff.
-			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-				if err != nil {
-					return err
-				}
+			if err = retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+				return err
 			}
 			continue
 		}
diff --git a/txnkv/txnsnapshot/snapshot.go b/txnkv/txnsnapshot/snapshot.go
index e81a871..4b34432 100644
--- a/txnkv/txnsnapshot/snapshot.go
+++ b/txnkv/txnsnapshot/snapshot.go
@@ -494,14 +494,8 @@ func (s *KVSnapshot) batchGetSingleRegion(bo *retry.Backoffer, batch batchKeys,
 		}
 		readType = req.ReadType
 		if regionErr != nil {
-			// For other region error and the fake region error, backoff because
-			// there's something wrong.
-			// For the real EpochNotMatch error, don't backoff.
-			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-				if err != nil {
-					return err
-				}
+			if err = retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+				return err
 			}
 			same, err := batch.relocate(bo, cli.regionCache)
 			if err != nil {
@@ -749,14 +743,8 @@ func (s *KVSnapshot) get(ctx context.Context, bo *retry.Backoffer, k []byte) ([]
 			return nil, err
 		}
 		if regionErr != nil {
-			// For other region error and the fake region error, backoff because
-			// there's something wrong.
-			// For the real EpochNotMatch error, don't backoff.
-			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
-				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
-				if err != nil {
-					return nil, err
-				}
+			if err = retry.MayBackoffForRegionError(regionErr, bo); err != nil {
+				return nil, err
 			}
 			continue
 		}


Test patch:
diff --git a/config/retry/backoff_test.go b/config/retry/backoff_test.go
index 84868fd..83feeb2 100644
--- a/config/retry/backoff_test.go
+++ b/config/retry/backoff_test.go
@@ -39,6 +39,8 @@ import (
 	"errors"
 	"testing"
 
+	"github.com/pingcap/kvproto/pkg/errorpb"
+	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -132,3 +134,49 @@ func TestBackoffWithMaxExcludedExceed(t *testing.T) {
 	assert.NotNil(t, err)
 	assert.Greater(t, b.excludedSleep, b.maxSleep)
 }
+
+func TestMayBackoffForRegionError(t *testing.T) {
+	// errors should retry without backoff
+	for _, regionErr := range []*errorpb.Error{
+		{
+			EpochNotMatch: &errorpb.EpochNotMatch{CurrentRegions: []*metapb.Region{
+				{Id: 1},
+			}},
+		},
+	} {
+		b := NewBackofferWithVars(context.TODO(), 1, nil)
+		err := MayBackoffForRegionError(regionErr, b)
+		assert.NoError(t, err, regionErr)
+		assert.Zero(t, b.totalSleep, regionErr)
+	}
+
+	// errors should back off and retry
+	for _, regionErr := range []*errorpb.Error{
+		{
+			// faked regionError
+			EpochNotMatch: &errorpb.EpochNotMatch{},
+		},
+		{
+			NotLeader: &errorpb.NotLeader{},
+		},
+		{
+			ServerIsBusy: &errorpb.ServerIsBusy{},
+		},
+		{
+			MaxTimestampNotSynced: &errorpb.MaxTimestampNotSynced{},
+		},
+	} {
+		// backoff succeeds
+		ctx, cancel := context.WithCancel(context.TODO())
+		b := NewBackofferWithVars(ctx, 1, nil)
+		err := MayBackoffForRegionError(regionErr, b)
+		assert.NoError(t, err, regionErr)
+		assert.Greater(t, b.totalSleep, 0, regionErr)
+
+		// backoff fails
+		cancel()
+		b = NewBackofferWithVars(ctx, 1, nil)
+		err = MayBackoffForRegionError(regionErr, b)
+		assert.EqualError(t, err, regionErr.String())
+	}
+}
diff --git a/integration_tests/1pc_test.go b/integration_tests/1pc_test.go
index c69bbdb..7dfe3da 100644
--- a/integration_tests/1pc_test.go
+++ b/integration_tests/1pc_test.go
@@ -38,6 +38,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/pingcap/failpoint"
 	"github.com/stretchr/testify/suite"
 	"github.com/tikv/client-go/v2/metrics"
 	"github.com/tikv/client-go/v2/oracle"
@@ -306,3 +307,17 @@ func (s *testOnePCSuite) TestTxnCommitCounter() {
 	s.Equal(diff.AsyncCommit, int64(1))
 	s.Equal(diff.OnePC, int64(1))
 }
+
+func (s *testOnePCSuite) TestFailWithUndeterminedResult() {
+	// commit primary fail for an undetermined result should return undetermined error
+	txn := s.begin()
+	s.Nil(txn.Set([]byte("key"), []byte("value")))
+	// prewrite fail for an undetermined result in commit should retry
+	s.Nil(failpoint.Enable(
+		"tikvclient/tikvStoreSendReqResult",
+		// prewrite success, but the first commit fail
+		`1*return("UndeterminedResult")->return("")`,
+	))
+	err := txn.Commit(context.Background())
+	s.Nil(err)
+}
diff --git a/integration_tests/2pc_test.go b/integration_tests/2pc_test.go
index fdad3b2..9483f9b 100644
--- a/integration_tests/2pc_test.go
+++ b/integration_tests/2pc_test.go
@@ -2769,3 +2769,28 @@ func (s *testCommitterSuite) Test2PCCleanupLifecycleHooks() {
 	wg.Wait()
 	s.Equal(reachedPost.Load(), true)
 }
+
+func (s *testCommitterSuite) TestFailWithUndeterminedResult() {
+	txn := s.begin()
+	s.Nil(txn.Set([]byte("key"), []byte("value")))
+	// prewrite fail for an undetermined result in commit should retry
+	s.Nil(failpoint.Enable(
+		"tikvclient/tikvStoreSendReqResult",
+		// prewrite fail, but retry success
+		`1*return("UndeterminedResult")->return("")`,
+	))
+	err := txn.Commit(context.Background())
+	s.Nil(err)
+
+	// commit primary fail for an undetermined result should return undetermined error
+	txn = s.begin()
+	s.Nil(txn.Set([]byte("key"), []byte("value")))
+	// prewrite fail for an undetermined result in commit should retry
+	s.Nil(failpoint.Enable(
+		"tikvclient/tikvStoreSendReqResult",
+		// prewrite success, but the first commit fail
+		`1*return("")->1*return("UndeterminedResult")->return("")`,
+	))
+	err = txn.Commit(context.Background())
+	s.Nil(err)
+}
diff --git a/integration_tests/async_commit_fail_test.go b/integration_tests/async_commit_fail_test.go
index c8cd703..b9edf1b 100644
--- a/integration_tests/async_commit_fail_test.go
+++ b/integration_tests/async_commit_fail_test.go
@@ -264,3 +264,13 @@ func (s *testAsyncCommitFailSuite) TestAsyncCommitContextCancelCausingUndetermin
 	s.NotNil(err)
 	s.NotNil(txn.GetCommitter().GetUndeterminedErr())
 }
+
+func (s *testAsyncCommitFailSuite) TestPrewriteFailWithUndeterminedResult() {
+	txn := s.beginAsyncCommit()
+	s.Nil(txn.Set([]byte("key"), []byte("value")))
+	// prewrite fail for an undetermined result in async commit should return undetermined error.
+	s.Nil(failpoint.Enable("tikvclient/tikvStoreSendReqResult", `1*return("UndeterminedResult")->return("")`))
+	err := txn.Commit(context.Background())
+	s.NotNil(err)
+	s.True(tikverr.IsErrorUndetermined(err))
+}
diff --git a/integration_tests/go.mod b/integration_tests/go.mod
index 059ff41..b838e9f 100644
--- a/integration_tests/go.mod
+++ b/integration_tests/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/ninedraft/israce v0.0.3
 	github.com/pingcap/errors v0.11.5-0.20241219054535-6b8c588c3122
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86
-	github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc
+	github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb
 	github.com/pingcap/tidb v1.1.0-beta.0.20250328040912-206ce6a49cac
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.10.0
diff --git a/integration_tests/go.sum b/integration_tests/go.sum
index 93a6664..1cd6a4c 100644
--- a/integration_tests/go.sum
+++ b/integration_tests/go.sum
@@ -1351,8 +1351,8 @@ github.com/pingcap/fn v1.0.0/go.mod h1:u9WZ1ZiOD1RpNhcI42RucFh/lBuzTu6rw88a+oF2Z
 github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989 h1:surzm05a8C9dN8dIUmo4Be2+pMRb6f55i+UIYrluu2E=
 github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw=
 github.com/pingcap/kvproto v0.0.0-20241113043844-e1fa7ea8c302/go.mod h1:rXxWk2UnwfUhLXha1jxRWPADw9eMZGWEWCg92Tgmb/8=
-github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc h1:rNjlA9TDyZcJl8JSGhpGIpDewLWhqihWGPv/ExSenlQ=
-github.com/pingcap/kvproto v0.0.0-20250427115124-e89bec5a53cc/go.mod h1:rXxWk2UnwfUhLXha1jxRWPADw9eMZGWEWCg92Tgmb/8=
+github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb h1:cTPTugbdIV2yklyOM95nqhMn7S5PvGgzKykOo/f8VtE=
+github.com/pingcap/kvproto v0.0.0-20250516034819-e2a9bdfc70eb/go.mod h1:rXxWk2UnwfUhLXha1jxRWPADw9eMZGWEWCg92Tgmb/8=
 github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7/go.mod h1:8AanEdAHATuRurdGxZXBz0At+9avep+ub7U1AGYLIMM=
 github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
 github.com/pingcap/log v1.1.1-0.20241212030209-7e3ff8601a2a h1:WIhmJBlNGmnCWH6TLMdZfNEDaiU8cFpZe3iaqDbQ0M8=
diff --git a/internal/locate/region_request3_test.go b/internal/locate/region_request3_test.go
index e7a5070..a299c1d 100644
--- a/internal/locate/region_request3_test.go
+++ b/internal/locate/region_request3_test.go
@@ -720,7 +720,7 @@ func (s *testRegionRequestToThreeStoresSuite) TestSendReqWithReplicaSelector() {
 		if err != nil {
 			return false
 		}
-		return IsFakeRegionError(regionErr)
+		return retry.IsFakeRegionError(regionErr)
 	}
 
 	// Normal
@@ -1167,7 +1167,7 @@ func (s *testRegionRequestToThreeStoresSuite) TestSendReqFirstTimeout() {
 			s.Nil(err)
 			regionErr, err := resp.GetRegionError()
 			s.Nil(err)
-			s.True(IsFakeRegionError(regionErr))
+			s.True(retry.IsFakeRegionError(regionErr))
 			s.Equal(1, s.regionRequestSender.Stats.GetRPCStatsCount())
 			s.Equal(uint32(3), s.regionRequestSender.Stats.GetCmdRPCCount(tikvrpc.CmdGet)) // 3 rpc
 			s.Equal(3, len(reqTargetAddrs))                                                // each rpc to a different store.
@@ -1491,7 +1491,7 @@ func (s *testRegionRequestToThreeStoresSuite) TestPreferLeader() {
 	s.NoError(err)
 	regionErr, err = resp.GetRegionError()
 	s.NoError(err)
-	s.True(IsFakeRegionError(regionErr))
+	s.True(retry.IsFakeRegionError(regionErr))
 }
 
 func (s *testRegionRequestToThreeStoresSuite) TestLeaderStuck() {
diff --git a/internal/locate/region_request_test.go b/internal/locate/region_request_test.go
index 117ff49..e51579f 100644
--- a/internal/locate/region_request_test.go
+++ b/internal/locate/region_request_test.go
@@ -772,7 +772,7 @@ func (s *testRegionRequestToSingleStoreSuite) TestKVReadTimeoutWithDisableBatchC
 	s.Nil(err)
 	s.NotNil(resp)
 	regionErr, _ := resp.GetRegionError()
-	s.True(IsFakeRegionError(regionErr))
+	s.True(retry.IsFakeRegionError(regionErr))
 	s.Equal(0, bo.GetTotalBackoffTimes()) // use kv read timeout will do fast retry, so backoff times should be 0.
 }
 
diff --git a/internal/locate/replica_selector_test.go b/internal/locate/replica_selector_test.go
index 980a4cf..c7f267b 100644
--- a/internal/locate/replica_selector_test.go
+++ b/internal/locate/replica_selector_test.go
@@ -3225,7 +3225,7 @@ func TestTiKVClientReadTimeout(t *testing.T) {
 	s.Nil(err)
 	s.NotNil(resp)
 	regionErr, _ := resp.GetRegionError()
-	s.True(IsFakeRegionError(regionErr))
+	s.True(retry.IsFakeRegionError(regionErr))
 	s.Equal(0, bo.GetTotalBackoffTimes())
 	s.Equal([]string{
 		"store1", "{addr: store1, replica-read: false, stale-read: false, timeout: 1}",