github.com/KinWaiYuen/client-go/v2@v2.5.4/txnkv/transaction/commit.go (about)

     1  // Copyright 2021 TiKV Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // NOTE: The code in this file is based on code from the
    16  // TiDB project, licensed under the Apache License v 2.0
    17  //
    18  // https://github.com/pingcap/tidb/tree/cc5e161ac06827589c4966674597c137cc9e809c/store/tikv/commit.go
    19  //
    20  
    21  // Copyright 2020 PingCAP, Inc.
    22  //
    23  // Licensed under the Apache License, Version 2.0 (the "License");
    24  // you may not use this file except in compliance with the License.
    25  // You may obtain a copy of the License at
    26  //
    27  //     http://www.apache.org/licenses/LICENSE-2.0
    28  //
    29  // Unless required by applicable law or agreed to in writing, software
    30  // distributed under the License is distributed on an "AS IS" BASIS,
    31  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    32  // See the License for the specific language governing permissions and
    33  // limitations under the License.
    34  
    35  package transaction
    36  
    37  import (
    38  	"encoding/hex"
    39  	"time"
    40  
    41  	tikverr "github.com/KinWaiYuen/client-go/v2/error"
    42  	"github.com/KinWaiYuen/client-go/v2/internal/client"
    43  	"github.com/KinWaiYuen/client-go/v2/internal/locate"
    44  	"github.com/KinWaiYuen/client-go/v2/internal/logutil"
    45  	"github.com/KinWaiYuen/client-go/v2/internal/retry"
    46  	"github.com/KinWaiYuen/client-go/v2/metrics"
    47  	"github.com/KinWaiYuen/client-go/v2/tikvrpc"
    48  	"github.com/opentracing/opentracing-go"
    49  	"github.com/pingcap/errors"
    50  	"github.com/pingcap/kvproto/pkg/kvrpcpb"
    51  	"github.com/prometheus/client_golang/prometheus"
    52  	"go.uber.org/zap"
    53  )
    54  
    55  type actionCommit struct{ retry bool }
    56  
    57  var _ twoPhaseCommitAction = actionCommit{}
    58  
    59  func (actionCommit) String() string {
    60  	return "commit"
    61  }
    62  
    63  func (actionCommit) tiKVTxnRegionsNumHistogram() prometheus.Observer {
    64  	return metrics.TxnRegionsNumHistogramCommit
    65  }
    66  
    67  func (actionCommit) handleSingleBatch(c *twoPhaseCommitter, bo *retry.Backoffer, batch batchMutations) error {
    68  	keys := batch.mutations.GetKeys()
    69  	req := tikvrpc.NewRequest(tikvrpc.CmdCommit, &kvrpcpb.CommitRequest{
    70  		StartVersion:  c.startTS,
    71  		Keys:          keys,
    72  		CommitVersion: c.commitTS,
    73  	}, kvrpcpb.Context{Priority: c.priority, SyncLog: c.syncLog,
    74  		ResourceGroupTag: c.resourceGroupTag, DiskFullOpt: c.diskFullOpt,
    75  		MaxExecutionDurationMs: uint64(client.MaxWriteExecutionTime.Milliseconds())})
    76  
    77  	tBegin := time.Now()
    78  	attempts := 0
    79  
    80  	sender := locate.NewRegionRequestSender(c.store.GetRegionCache(), c.store.GetTiKVClient())
    81  	for {
    82  		attempts++
    83  		if time.Since(tBegin) > slowRequestThreshold {
    84  			logutil.BgLogger().Warn("slow commit request", zap.Uint64("startTS", c.startTS), zap.Stringer("region", &batch.region), zap.Int("attempts", attempts))
    85  			tBegin = time.Now()
    86  		}
    87  
    88  		resp, err := sender.SendReq(bo, req, batch.region, client.ReadTimeoutShort)
    89  		// If we fail to receive response for the request that commits primary key, it will be undetermined whether this
    90  		// transaction has been successfully committed.
    91  		// Under this circumstance, we can not declare the commit is complete (may lead to data lost), nor can we throw
    92  		// an error (may lead to the duplicated key error when upper level restarts the transaction). Currently the best
    93  		// solution is to populate this error and let upper layer drop the connection to the corresponding mysql client.
    94  		if batch.isPrimary && sender.GetRPCError() != nil && !c.isAsyncCommit() {
    95  			c.setUndeterminedErr(errors.Trace(sender.GetRPCError()))
    96  		}
    97  
    98  		// Unexpected error occurs, return it.
    99  		if err != nil {
   100  			return errors.Trace(err)
   101  		}
   102  
   103  		regionErr, err := resp.GetRegionError()
   104  		if err != nil {
   105  			return errors.Trace(err)
   106  		}
   107  		if regionErr != nil {
   108  			// For other region error and the fake region error, backoff because
   109  			// there's something wrong.
   110  			// For the real EpochNotMatch error, don't backoff.
   111  			if regionErr.GetEpochNotMatch() == nil || locate.IsFakeRegionError(regionErr) {
   112  				err = bo.Backoff(retry.BoRegionMiss, errors.New(regionErr.String()))
   113  				if err != nil {
   114  					return errors.Trace(err)
   115  				}
   116  			}
   117  			same, err := batch.relocate(bo, c.store.GetRegionCache())
   118  			if err != nil {
   119  				return errors.Trace(err)
   120  			}
   121  			if same {
   122  				continue
   123  			}
   124  			err = c.doActionOnMutations(bo, actionCommit{true}, batch.mutations)
   125  			return errors.Trace(err)
   126  		}
   127  
   128  		if resp.Resp == nil {
   129  			return errors.Trace(tikverr.ErrBodyMissing)
   130  		}
   131  		commitResp := resp.Resp.(*kvrpcpb.CommitResponse)
   132  		// Here we can make sure tikv has processed the commit primary key request. So
   133  		// we can clean undetermined error.
   134  		if batch.isPrimary && !c.isAsyncCommit() {
   135  			c.setUndeterminedErr(nil)
   136  		}
   137  		if keyErr := commitResp.GetError(); keyErr != nil {
   138  			if rejected := keyErr.GetCommitTsExpired(); rejected != nil {
   139  				logutil.Logger(bo.GetCtx()).Info("2PC commitTS rejected by TiKV, retry with a newer commitTS",
   140  					zap.Uint64("txnStartTS", c.startTS),
   141  					zap.Stringer("info", logutil.Hex(rejected)))
   142  
   143  				// Do not retry for a txn which has a too large MinCommitTs
   144  				// 3600000 << 18 = 943718400000
   145  				if rejected.MinCommitTs-rejected.AttemptedCommitTs > 943718400000 {
   146  					err := errors.Errorf("2PC MinCommitTS is too large, we got MinCommitTS: %d, and AttemptedCommitTS: %d",
   147  						rejected.MinCommitTs, rejected.AttemptedCommitTs)
   148  					return errors.Trace(err)
   149  				}
   150  
   151  				// Update commit ts and retry.
   152  				commitTS, err := c.store.GetTimestampWithRetry(bo, c.txn.GetScope())
   153  				if err != nil {
   154  					logutil.Logger(bo.GetCtx()).Warn("2PC get commitTS failed",
   155  						zap.Error(err),
   156  						zap.Uint64("txnStartTS", c.startTS))
   157  					return errors.Trace(err)
   158  				}
   159  
   160  				c.mu.Lock()
   161  				c.commitTS = commitTS
   162  				c.mu.Unlock()
   163  				// Update the commitTS of the request and retry.
   164  				req.Commit().CommitVersion = commitTS
   165  				continue
   166  			}
   167  
   168  			c.mu.RLock()
   169  			defer c.mu.RUnlock()
   170  			err = tikverr.ExtractKeyErr(keyErr)
   171  			if c.mu.committed {
   172  				// No secondary key could be rolled back after it's primary key is committed.
   173  				// There must be a serious bug somewhere.
   174  				hexBatchKeys := func(keys [][]byte) []string {
   175  					var res []string
   176  					for _, k := range keys {
   177  						res = append(res, hex.EncodeToString(k))
   178  					}
   179  					return res
   180  				}
   181  				logutil.Logger(bo.GetCtx()).Error("2PC failed commit key after primary key committed",
   182  					zap.Error(err),
   183  					zap.Uint64("txnStartTS", c.startTS),
   184  					zap.Uint64("commitTS", c.commitTS),
   185  					zap.Strings("keys", hexBatchKeys(keys)))
   186  				return errors.Trace(err)
   187  			}
   188  			// The transaction maybe rolled back by concurrent transactions.
   189  			logutil.Logger(bo.GetCtx()).Debug("2PC failed commit primary key",
   190  				zap.Error(err),
   191  				zap.Uint64("txnStartTS", c.startTS))
   192  			return err
   193  		}
   194  		break
   195  	}
   196  
   197  	c.mu.Lock()
   198  	defer c.mu.Unlock()
   199  	// Group that contains primary key is always the first.
   200  	// We mark transaction's status committed when we receive the first success response.
   201  	c.mu.committed = true
   202  	return nil
   203  }
   204  
   205  func (c *twoPhaseCommitter) commitMutations(bo *retry.Backoffer, mutations CommitterMutations) error {
   206  	if span := opentracing.SpanFromContext(bo.GetCtx()); span != nil && span.Tracer() != nil {
   207  		span1 := span.Tracer().StartSpan("twoPhaseCommitter.commitMutations", opentracing.ChildOf(span.Context()))
   208  		defer span1.Finish()
   209  		bo.SetCtx(opentracing.ContextWithSpan(bo.GetCtx(), span1))
   210  	}
   211  
   212  	return c.doActionOnMutations(bo, actionCommit{}, mutations)
   213  }