github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvclient/kvcoord/txn_interceptor_heartbeater.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package kvcoord
    12  
    13  import (
    14  	"context"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    19  	"github.com/cockroachdb/cockroach/pkg/util/envutil"
    20  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    21  	"github.com/cockroachdb/cockroach/pkg/util/log"
    22  	"github.com/cockroachdb/cockroach/pkg/util/stop"
    23  	opentracing "github.com/opentracing/opentracing-go"
    24  )
    25  
    26  // txnHeartbeatDuring1PC defines whether the txnHeartbeater should launch a
    27  // heartbeat loop for 1PC transactions. The value defaults to false even though
    28  // 1PC transactions leave intents around on retriable errors if the batch has
    29  // been split between ranges and may be pushed when in lock wait-queues because
    30  // we consider that unlikely enough so we prefer to not pay for a goroutine.
    31  var txnHeartbeatFor1PC = envutil.EnvOrDefaultBool("COCKROACH_TXN_HEARTBEAT_DURING_1PC", false)
    32  
    33  // txnHeartbeater is a txnInterceptor in charge of a transaction's heartbeat
    34  // loop. Transaction coordinators heartbeat their transaction record
    35  // periodically to indicate the liveness of their transaction. Other actors like
    36  // concurrent transactions and GC processes observe a transaction record's last
    37  // heartbeat time to learn about its disposition and to determine whether it
    38  // should be considered abandoned. When a transaction is considered abandoned,
    39  // other actors are free to abort it at will. As such, it is important for a
    40  // transaction coordinator to heartbeat its transaction record with a
    41  // periodicity well below the abandonment threshold.
    42  //
    43  // Transaction coordinators only need to perform heartbeats for transactions
    44  // that risk running for longer than the abandonment duration. For transactions
    45  // that finish well beneath this time, a heartbeat will never be sent and the
    46  // EndTxn request will create and immediately finalize the transaction. However,
    47  // for transactions that live long enough that they risk running into issues
    48  // with other's perceiving them as abandoned, the first HeartbeatTxn request
    49  // they send will create the transaction record in the PENDING state. Future
    50  // heartbeats will update the transaction record to indicate progressively
    51  // larger heartbeat timestamps.
    52  //
    53  // NOTE: there are other mechanisms by which concurrent actors could determine
    54  // the liveness of transactions. One proposal is to have concurrent actors
    55  // communicate directly with transaction coordinators themselves. This would
    56  // avoid the need for transaction heartbeats and the PENDING transaction state
    57  // entirely. Another proposal is to detect abandoned transactions and failed
    58  // coordinators at an entirely different level - by maintaining a node health
    59  // plane. This would function under the idea that if the node a transaction's
    60  // coordinator is running on is alive then that transaction is still in-progress
    61  // unless it specifies otherwise. These are both approaches we could consider in
    62  // the future.
    63  type txnHeartbeater struct {
    64  	log.AmbientContext
    65  	stopper      *stop.Stopper
    66  	clock        *hlc.Clock
    67  	metrics      *TxnMetrics
    68  	loopInterval time.Duration
    69  
    70  	// wrapped is the next sender in the interceptor stack.
    71  	wrapped lockedSender
    72  	// gatekeeper is the sender to which heartbeat requests need to be sent. It is
    73  	// set to the gatekeeper interceptor, so sending directly to it will bypass
    74  	// all the other interceptors; heartbeats don't need them and they can only
    75  	// hurt - we don't want heartbeats to get sequence numbers or to check any
    76  	// intents. Note that the async rollbacks that this interceptor sometimes
    77  	// sends got through `wrapped`, not directly through `gatekeeper`.
    78  	gatekeeper lockedSender
    79  
    80  	// mu contains state protected by the TxnCoordSender's mutex.
    81  	mu struct {
    82  		sync.Locker
    83  
    84  		// txn is a reference to the TxnCoordSender's proto.
    85  		txn *roachpb.Transaction
    86  
    87  		// loopStarted indicates whether the heartbeat loop has been launched
    88  		// for the transaction or not. It remains true once the loop terminates.
    89  		loopStarted bool
    90  
    91  		// loopCancel is a function to cancel the context of the heartbeat loop.
    92  		// Non-nil if the heartbeat loop is currently running.
    93  		loopCancel func()
    94  
    95  		// finalObservedStatus is the finalized status that the heartbeat loop
    96  		// observed while heartbeating the transaction's record. As soon as the
    97  		// heartbeat loop observes a finalized status, it shuts down.
    98  		//
    99  		// If the status here is COMMITTED then the transaction definitely
   100  		// committed. However, if the status here is ABORTED then the
   101  		// transaction may or may not have been aborted. Instead, it's possible
   102  		// that the transaction was committed by an EndTxn request and then its
   103  		// record was garbage collected before the heartbeat request reached the
   104  		// record. The only way to distinguish this situation from a truly
   105  		// aborted transaction is to consider whether or not the transaction
   106  		// coordinator sent an EndTxn request and, if so, consider whether it
   107  		// succeeded or not.
   108  		//
   109  		// Because of this ambiguity, the status is not used to immediately
   110  		// update txn in case the heartbeat loop raced with an EndTxn request.
   111  		// Instead, it is used by the transaction coordinator to reject any
   112  		// future requests sent though it (which indicates that the heartbeat
   113  		// loop did not race with an EndTxn request).
   114  		finalObservedStatus roachpb.TransactionStatus
   115  	}
   116  }
   117  
   118  // init initializes the txnHeartbeater. This method exists instead of a
   119  // constructor because txnHeartbeaters live in a pool in the TxnCoordSender.
   120  func (h *txnHeartbeater) init(
   121  	ac log.AmbientContext,
   122  	stopper *stop.Stopper,
   123  	clock *hlc.Clock,
   124  	metrics *TxnMetrics,
   125  	loopInterval time.Duration,
   126  	gatekeeper lockedSender,
   127  	mu sync.Locker,
   128  	txn *roachpb.Transaction,
   129  ) {
   130  	h.AmbientContext = ac
   131  	h.stopper = stopper
   132  	h.clock = clock
   133  	h.metrics = metrics
   134  	h.loopInterval = loopInterval
   135  	h.gatekeeper = gatekeeper
   136  	h.mu.Locker = mu
   137  	h.mu.txn = txn
   138  }
   139  
   140  // SendLocked is part of the txnInterceptor interface.
   141  func (h *txnHeartbeater) SendLocked(
   142  	ctx context.Context, ba roachpb.BatchRequest,
   143  ) (*roachpb.BatchResponse, *roachpb.Error) {
   144  	firstLockingIndex, pErr := firstLockingIndex(&ba)
   145  	if pErr != nil {
   146  		return nil, pErr
   147  	}
   148  	if firstLockingIndex != -1 {
   149  		// Set txn key based on the key of the first transactional write if not
   150  		// already set. If it is already set, make sure we keep the anchor key
   151  		// the same.
   152  		if len(h.mu.txn.Key) == 0 {
   153  			anchor := ba.Requests[firstLockingIndex].GetInner().Header().Key
   154  			h.mu.txn.Key = anchor
   155  			// Put the anchor also in the ba's copy of the txn, since this batch
   156  			// was prepared before we had an anchor.
   157  			ba.Txn.Key = anchor
   158  		}
   159  
   160  		// Start the heartbeat loop if it has not already started.
   161  		if !h.mu.loopStarted {
   162  			_, haveEndTxn := ba.GetArg(roachpb.EndTxn)
   163  			if !haveEndTxn || txnHeartbeatFor1PC {
   164  				if err := h.startHeartbeatLoopLocked(ctx); err != nil {
   165  					return nil, roachpb.NewError(err)
   166  				}
   167  			}
   168  		}
   169  	}
   170  
   171  	// Forward the batch through the wrapped lockedSender.
   172  	return h.wrapped.SendLocked(ctx, ba)
   173  }
   174  
   175  // setWrapped is part of the txnInterceptor interface.
   176  func (h *txnHeartbeater) setWrapped(wrapped lockedSender) {
   177  	h.wrapped = wrapped
   178  }
   179  
   180  // populateLeafInputState is part of the txnInterceptor interface.
   181  func (*txnHeartbeater) populateLeafInputState(*roachpb.LeafTxnInputState) {}
   182  
   183  // populateLeafFinalState is part of the txnInterceptor interface.
   184  func (*txnHeartbeater) populateLeafFinalState(*roachpb.LeafTxnFinalState) {}
   185  
   186  // importLeafFinalState is part of the txnInterceptor interface.
   187  func (*txnHeartbeater) importLeafFinalState(context.Context, *roachpb.LeafTxnFinalState) {}
   188  
   189  // epochBumpedLocked is part of the txnInterceptor interface.
   190  func (h *txnHeartbeater) epochBumpedLocked() {}
   191  
   192  // createSavepointLocked is part of the txnReqInterceptor interface.
   193  func (*txnHeartbeater) createSavepointLocked(context.Context, *savepoint) {}
   194  
   195  // rollbackToSavepointLocked is part of the txnReqInterceptor interface.
   196  func (*txnHeartbeater) rollbackToSavepointLocked(context.Context, savepoint) {}
   197  
   198  // closeLocked is part of the txnInterceptor interface.
   199  func (h *txnHeartbeater) closeLocked() {
   200  	h.cancelHeartbeatLoopLocked()
   201  }
   202  
   203  // startHeartbeatLoopLocked starts a heartbeat loop in a different goroutine.
   204  func (h *txnHeartbeater) startHeartbeatLoopLocked(ctx context.Context) error {
   205  	if h.mu.loopStarted {
   206  		log.Fatal(ctx, "attempting to start a second heartbeat loop")
   207  	}
   208  	log.VEventf(ctx, 2, "coordinator spawns heartbeat loop")
   209  	h.mu.loopStarted = true
   210  	// NB: we can't do this in init() because the txn isn't populated yet then
   211  	// (it's zero).
   212  	h.AmbientContext.AddLogTag("txn-hb", h.mu.txn.Short())
   213  
   214  	// Create a new context so that the heartbeat loop doesn't inherit the
   215  	// caller's cancelation.
   216  	// We want the loop to run in a span linked to the current one, though, so we
   217  	// put our span in the new context and expect RunAsyncTask to fork it
   218  	// immediately.
   219  	hbCtx := h.AnnotateCtx(context.Background())
   220  	hbCtx = opentracing.ContextWithSpan(hbCtx, opentracing.SpanFromContext(ctx))
   221  	hbCtx, h.mu.loopCancel = context.WithCancel(hbCtx)
   222  
   223  	return h.stopper.RunAsyncTask(hbCtx, "kv.TxnCoordSender: heartbeat loop", h.heartbeatLoop)
   224  }
   225  
   226  func (h *txnHeartbeater) cancelHeartbeatLoopLocked() {
   227  	// If the heartbeat loop has already started, cancel it.
   228  	if h.heartbeatLoopRunningLocked() {
   229  		h.mu.loopCancel()
   230  		h.mu.loopCancel = nil
   231  	}
   232  }
   233  
   234  func (h *txnHeartbeater) heartbeatLoopRunningLocked() bool {
   235  	return h.mu.loopCancel != nil
   236  }
   237  
   238  // heartbeatLoop periodically sends a HeartbeatTxn request to the transaction
   239  // record, stopping in the event the transaction is aborted or committed after
   240  // attempting to resolve the intents.
   241  func (h *txnHeartbeater) heartbeatLoop(ctx context.Context) {
   242  	defer func() {
   243  		h.mu.Lock()
   244  		h.cancelHeartbeatLoopLocked()
   245  		h.mu.Unlock()
   246  	}()
   247  
   248  	var tickChan <-chan time.Time
   249  	{
   250  		ticker := time.NewTicker(h.loopInterval)
   251  		tickChan = ticker.C
   252  		defer ticker.Stop()
   253  	}
   254  
   255  	// Loop with ticker for periodic heartbeats.
   256  	for {
   257  		select {
   258  		case <-tickChan:
   259  			if !h.heartbeat(ctx) {
   260  				// The heartbeat noticed a finalized transaction,
   261  				// so shut down the heartbeat loop.
   262  				return
   263  			}
   264  		case <-ctx.Done():
   265  			// Transaction finished normally.
   266  			return
   267  		case <-h.stopper.ShouldQuiesce():
   268  			return
   269  		}
   270  	}
   271  }
   272  
   273  // heartbeat sends a HeartbeatTxnRequest to the txn record.
   274  // Returns true if heartbeating should continue, false if the transaction is no
   275  // longer Pending and so there's no point in heartbeating further.
   276  func (h *txnHeartbeater) heartbeat(ctx context.Context) bool {
   277  	// Like with the TxnCoordSender, the locking here is peculiar. The lock is not
   278  	// held continuously throughout this method: we acquire the lock here and
   279  	// then, inside the wrapped.Send() call, the interceptor at the bottom of the
   280  	// stack will unlock until it receives a response.
   281  	h.mu.Lock()
   282  	defer h.mu.Unlock()
   283  
   284  	// The heartbeat loop might have raced with the cancelation of the heartbeat.
   285  	if ctx.Err() != nil {
   286  		return false
   287  	}
   288  
   289  	if h.mu.txn.Status != roachpb.PENDING {
   290  		if h.mu.txn.Status == roachpb.COMMITTED {
   291  			log.Fatalf(ctx, "txn committed but heartbeat loop hasn't been signaled to stop: %s", h.mu.txn)
   292  		}
   293  		// If the transaction is aborted, there's no point in heartbeating. The
   294  		// client needs to send a rollback.
   295  		return false
   296  	}
   297  
   298  	// Clone the txn in order to put it in the heartbeat request.
   299  	txn := h.mu.txn.Clone()
   300  	if txn.Key == nil {
   301  		log.Fatalf(ctx, "attempting to heartbeat txn without anchor key: %v", txn)
   302  	}
   303  	ba := roachpb.BatchRequest{}
   304  	ba.Txn = txn
   305  	ba.Add(&roachpb.HeartbeatTxnRequest{
   306  		RequestHeader: roachpb.RequestHeader{
   307  			Key: txn.Key,
   308  		},
   309  		Now: h.clock.Now(),
   310  	})
   311  
   312  	// Send the heartbeat request directly through the gatekeeper interceptor.
   313  	// See comment on h.gatekeeper for a discussion of why.
   314  	log.VEvent(ctx, 2, "heartbeat")
   315  	br, pErr := h.gatekeeper.SendLocked(ctx, ba)
   316  
   317  	// If the txn is no longer pending, ignore the result of the heartbeat
   318  	// and tear down the heartbeat loop.
   319  	if h.mu.txn.Status != roachpb.PENDING {
   320  		return false
   321  	}
   322  
   323  	var respTxn *roachpb.Transaction
   324  	if pErr != nil {
   325  		log.VEventf(ctx, 2, "heartbeat failed: %s", pErr)
   326  
   327  		// We need to be prepared here to handle the case of a
   328  		// TransactionAbortedError with no transaction proto in it.
   329  		//
   330  		// TODO(nvanbenschoten): Make this the only case where we get back an
   331  		// Aborted txn.
   332  		if _, ok := pErr.GetDetail().(*roachpb.TransactionAbortedError); ok {
   333  			// Note that it's possible that the txn actually committed but its
   334  			// record got GC'ed. In that case, aborting won't hurt anyone though,
   335  			// since all intents have already been resolved.
   336  			// The only thing we must ascertain is that we don't tell the client
   337  			// about this error - it will get either a definitive result of
   338  			// its commit or an ambiguous one and we have nothing to offer that
   339  			// provides more clarity. We do however prevent it from running more
   340  			// requests in case it isn't aware that the transaction is over.
   341  			h.abortTxnAsyncLocked(ctx)
   342  			h.mu.finalObservedStatus = roachpb.ABORTED
   343  			return false
   344  		}
   345  
   346  		respTxn = pErr.GetTxn()
   347  	} else {
   348  		respTxn = br.Txn
   349  	}
   350  
   351  	// Tear down the heartbeat loop if the response transaction is finalized.
   352  	if respTxn != nil && respTxn.Status.IsFinalized() {
   353  		switch respTxn.Status {
   354  		case roachpb.COMMITTED:
   355  			// Shut down the heartbeat loop without doing anything else.
   356  			// We must have raced with an EndTxn(commit=true).
   357  		case roachpb.ABORTED:
   358  			// Roll back the transaction record to clean up intents and
   359  			// then shut down the heartbeat loop.
   360  			h.abortTxnAsyncLocked(ctx)
   361  		}
   362  		h.mu.finalObservedStatus = respTxn.Status
   363  		return false
   364  	}
   365  	return true
   366  }
   367  
   368  // abortTxnAsyncLocked send an EndTxn(commmit=false) asynchronously.
   369  // The purpose of the async cleanup is to resolve transaction intents as soon
   370  // as possible when a transaction coordinator observes an ABORTED transaction.
   371  func (h *txnHeartbeater) abortTxnAsyncLocked(ctx context.Context) {
   372  	log.VEventf(ctx, 1, "Heartbeat detected aborted txn. Cleaning up.")
   373  
   374  	// NB: We use context.Background() here because we don't want a canceled
   375  	// context to interrupt the aborting.
   376  	ctx = h.AnnotateCtx(context.Background())
   377  
   378  	// Construct a batch with an EndTxn request.
   379  	txn := h.mu.txn.Clone()
   380  	ba := roachpb.BatchRequest{}
   381  	ba.Header = roachpb.Header{Txn: txn}
   382  	ba.Add(&roachpb.EndTxnRequest{
   383  		Commit: false,
   384  		// Resolved intents should maintain an abort span entry to prevent
   385  		// concurrent requests from failing to notice the transaction was aborted.
   386  		Poison: true,
   387  	})
   388  
   389  	log.VEventf(ctx, 2, "async abort for txn: %s", txn)
   390  	if err := h.stopper.RunAsyncTask(
   391  		ctx, "txnHeartbeater: aborting txn", func(ctx context.Context) {
   392  			// Send the abort request through the interceptor stack. This is
   393  			// important because we need the txnPipeliner to append lock spans
   394  			// to the EndTxn request.
   395  			h.mu.Lock()
   396  			defer h.mu.Unlock()
   397  			_, pErr := h.wrapped.SendLocked(ctx, ba)
   398  			if pErr != nil {
   399  				log.VErrEventf(ctx, 1, "async abort failed for %s: %s ", txn, pErr)
   400  			}
   401  		},
   402  	); err != nil {
   403  		log.Warningf(ctx, "%v", err)
   404  	}
   405  }
   406  
   407  // firstLockingIndex returns the index of the first request that acquires locks
   408  // in the BatchRequest. Returns -1 if the batch has no intention to acquire
   409  // locks. It also verifies that if an EndTxnRequest is included, then it is the
   410  // last request in the batch.
   411  func firstLockingIndex(ba *roachpb.BatchRequest) (int, *roachpb.Error) {
   412  	for i, ru := range ba.Requests {
   413  		args := ru.GetInner()
   414  		if i < len(ba.Requests)-1 /* if not last*/ {
   415  			if _, ok := args.(*roachpb.EndTxnRequest); ok {
   416  				return -1, roachpb.NewErrorf("%s sent as non-terminal call", args.Method())
   417  			}
   418  		}
   419  		if roachpb.IsLocking(args) {
   420  			return i, nil
   421  		}
   422  	}
   423  	return -1, nil
   424  }