// TestTxnCoordSenderSingleRoundtripTxn checks that a batch which completely // holds the writing portion of a Txn (including EndTransaction) does not // launch a heartbeat goroutine at all. func TestTxnCoordSenderSingleRoundtripTxn(t *testing.T) { defer leaktest.AfterTest(t) stopper := stop.NewStopper() manual := hlc.NewManualClock(0) clock := hlc.NewClock(manual.UnixNano) clock.SetMaxOffset(20) ts := NewTxnCoordSender(senderFn(func(_ context.Context, ba proto.BatchRequest) (*proto.BatchResponse, *proto.Error) { return ba.CreateReply().(*proto.BatchResponse), nil }), clock, false, nil, stopper) // Stop the stopper manually, prior to trying the transaction. This has the // effect of returning a NodeUnavailableError for any attempts at launching // a heartbeat goroutine. stopper.Stop() var ba proto.BatchRequest put := &proto.PutRequest{} put.Key = proto.Key("test") ba.Add(put) ba.Add(&proto.EndTransactionRequest{}) ba.Txn = &proto.Transaction{Name: "test"} _, pErr := ts.Send(context.Background(), ba) if pErr != nil { t.Fatal(pErr) } }
func (ts *txnSender) Send(ctx context.Context, ba proto.BatchRequest) (*proto.BatchResponse, *proto.Error) { // Send call through wrapped sender. ba.Txn = &ts.Proto br, pErr := ts.wrapped.Send(ctx, ba) if br != nil && br.Error != nil { panic(proto.ErrorUnexpectedlySet(ts.wrapped, br)) } // TODO(tschottdorf): see about using only the top-level *proto.Error // information for this restart logic (includes adding the Txn). err := pErr.GoError() // Only successful requests can carry an updated Txn in their response // header. Any error (e.g. a restart) can have a Txn attached to them as // well; those update our local state in the same way for the next attempt. // The exception is if our transaction was aborted and needs to restart // from scratch, in which case we do just that. if err == nil { ts.Proto.Update(br.Txn) return br, nil } else if abrtErr, ok := err.(*proto.TransactionAbortedError); ok { // On Abort, reset the transaction so we start anew on restart. ts.Proto = proto.Transaction{ Name: ts.Proto.Name, Isolation: ts.Proto.Isolation, // Acts as a minimum priority on restart. Priority: abrtErr.Transaction().GetPriority(), } } else if txnErr, ok := err.(proto.TransactionRestartError); ok { ts.Proto.Update(txnErr.Transaction()) } return nil, pErr }
func (tc *TxnCoordSender) heartbeat(id string, trace *tracer.Trace, ctx context.Context) bool { tc.Lock() proceed := true txnMeta := tc.txns[id] // Before we send a heartbeat, determine whether this transaction // should be considered abandoned. If so, exit heartbeat. if txnMeta.hasClientAbandonedCoord(tc.clock.PhysicalNow()) { // TODO(tschottdorf): should we be more proactive here? // The client might be continuing the transaction // through another coordinator, but in the most likely // case it's just gone and the open transaction record // could block concurrent operations. if log.V(1) { log.Infof("transaction %s abandoned; stopping heartbeat", txnMeta.txn) } proceed = false } // txnMeta.txn is possibly replaced concurrently, // so grab a copy before unlocking. txn := txnMeta.txn tc.Unlock() if !proceed { return false } hb := &proto.HeartbeatTxnRequest{} hb.Key = txn.Key ba := proto.BatchRequest{} ba.Timestamp = tc.clock.Now() ba.Key = txn.Key ba.Txn = &txn ba.Add(hb) epochEnds := trace.Epoch("heartbeat") _, err := tc.wrapped.Send(ctx, ba) epochEnds() // If the transaction is not in pending state, then we can stop // the heartbeat. It's either aborted or committed, and we resolve // write intents accordingly. if err != nil { log.Warningf("heartbeat to %s failed: %s", txn, err) } // TODO(bdarnell): once we have gotten a heartbeat response with // Status != PENDING, future heartbeats are useless. However, we // need to continue the heartbeatLoop until the client either // commits or abandons the transaction. We could save a little // pointless work by restructuring this loop to stop sending // heartbeats between the time that the transaction is aborted and // the client finds out. Furthermore, we could use this information // to send TransactionAbortedErrors to the client so it can restart // immediately instead of running until its EndTransaction. return true }
// maybeBeginTxn begins a new transaction if a txn has been specified // in the request but has a nil ID. The new transaction is initialized // using the name and isolation in the otherwise uninitialized txn. // The Priority, if non-zero is used as a minimum. func (tc *TxnCoordSender) maybeBeginTxn(ba *proto.BatchRequest) { if ba.Txn == nil { return } if len(ba.Requests) == 0 { panic("empty batch with txn") } if len(ba.Txn.ID) == 0 { // TODO(tschottdorf): should really choose the first txn write here. firstKey := ba.Requests[0].GetInner().Header().Key newTxn := proto.NewTransaction(ba.Txn.Name, keys.KeyAddress(firstKey), ba.GetUserPriority(), ba.Txn.Isolation, tc.clock.Now(), tc.clock.MaxOffset().Nanoseconds()) // Use existing priority as a minimum. This is used on transaction // aborts to ratchet priority when creating successor transaction. if newTxn.Priority < ba.Txn.Priority { newTxn.Priority = ba.Txn.Priority } ba.Txn = newTxn } }