// maybeRejectClientLocked checks whether the (transactional) request is in a // state that prevents it from continuing, such as the coordinator having // considered the client abandoned, or a heartbeat having reported an error. func (tc *TxnCoordSender) maybeRejectClientLocked( ctx context.Context, txn roachpb.Transaction, ) *roachpb.Error { if !txn.Writing { return nil } txnMeta, ok := tc.txns[*txn.ID] // Check whether the transaction is still tracked and has a chance of // completing. It's possible that the coordinator learns about the // transaction having terminated from a heartbeat, and GC queue correctness // (along with common sense) mandates that we don't let the client // continue. switch { case !ok: log.VEventf(ctx, 2, "rejecting unknown txn: %s", txn.ID) // TODO(spencerkimball): Could add coordinator node ID to the // transaction session so that we can definitively return the right // error between these possible errors. Or update the code to make an // educated guess based on the incoming transaction timestamp. return roachpb.NewError(errNoState) case txnMeta.txn.Status == roachpb.ABORTED: txn := txnMeta.txn.Clone() tc.cleanupTxnLocked(ctx, txn) return roachpb.NewErrorWithTxn(roachpb.NewTransactionAbortedError(), &txn) case txnMeta.txn.Status == roachpb.COMMITTED: txn := txnMeta.txn.Clone() tc.cleanupTxnLocked(ctx, txn) return roachpb.NewErrorWithTxn(roachpb.NewTransactionStatusError( "transaction is already committed"), &txn) default: return nil } }
func TestAbortCountConflictingWrites(t *testing.T) { defer leaktest.AfterTest(t)() params, cmdFilters := createTestServerParams() s, sqlDB, _ := serverutils.StartServer(t, params) defer s.Stopper().Stop() if _, err := sqlDB.Exec("CREATE DATABASE db"); err != nil { t.Fatal(err) } if _, err := sqlDB.Exec("CREATE TABLE db.t (k TEXT PRIMARY KEY, v TEXT)"); err != nil { t.Fatal(err) } // Inject errors on the INSERT below. restarted := false cmdFilters.AppendFilter(func(args storagebase.FilterArgs) *roachpb.Error { switch req := args.Req.(type) { // SQL INSERT generates ConditionalPuts for unique indexes (such as the PK). case *roachpb.ConditionalPutRequest: if bytes.Contains(req.Value.RawBytes, []byte("marker")) && !restarted { restarted = true return roachpb.NewErrorWithTxn( roachpb.NewTransactionAbortedError(), args.Hdr.Txn) } } return nil }, false) txn, err := sqlDB.Begin() if err != nil { t.Fatal(err) } _, err = txn.Exec("INSERT INTO db.t VALUES ('key', 'marker')") if !testutils.IsError(err, "aborted") { t.Fatal(err) } if err = txn.Rollback(); err != nil { t.Fatal(err) } if err := checkCounterEQ(s, sql.MetaTxnAbort, 1); err != nil { t.Error(err) } if err := checkCounterEQ(s, sql.MetaTxnBegin, 1); err != nil { t.Error(err) } if err := checkCounterEQ(s, sql.MetaTxnRollback, 0); err != nil { t.Error(err) } if err := checkCounterEQ(s, sql.MetaTxnCommit, 0); err != nil { t.Error(err) } if err := checkCounterEQ(s, sql.MetaInsert, 1); err != nil { t.Error(err) } }
func injectErrors(req roachpb.Request, hdr roachpb.Header, magicVals *filterVals) error { magicVals.Lock() defer magicVals.Unlock() switch req := req.(type) { case *roachpb.ConditionalPutRequest: for key, count := range magicVals.restartCounts { if err := checkCorrectTxn(string(req.Value.RawBytes), magicVals, hdr.Txn); err != nil { return err } if count > 0 && bytes.Contains(req.Value.RawBytes, []byte(key)) { magicVals.restartCounts[key]-- err := roachpb.NewReadWithinUncertaintyIntervalError( hlc.ZeroTimestamp, hlc.ZeroTimestamp) magicVals.failedValues[string(req.Value.RawBytes)] = failureRecord{err, hdr.Txn} return err } } for key, count := range magicVals.abortCounts { if err := checkCorrectTxn(string(req.Value.RawBytes), magicVals, hdr.Txn); err != nil { return err } if count > 0 && bytes.Contains(req.Value.RawBytes, []byte(key)) { magicVals.abortCounts[key]-- err := roachpb.NewTransactionAbortedError() magicVals.failedValues[string(req.Value.RawBytes)] = failureRecord{err, hdr.Txn} return err } } return nil default: return nil } }
// send runs the specified calls synchronously in a single batch and // returns any errors. If the transaction is read-only or has already // been successfully committed or aborted, a potential trailing // EndTransaction call is silently dropped, allowing the caller to // always commit or clean-up explicitly even when that may not be // required (or even erroneous). Returns (nil, nil) for an empty batch. func (txn *Txn) send(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) { if txn.Proto.Status != roachpb.PENDING || txn.IsFinalized() { return nil, roachpb.NewErrorf( "attempting to use transaction with wrong status or finalized: %s", txn.Proto.Status) } // It doesn't make sense to use inconsistent reads in a transaction. However, // we still need to accept it as a parameter for this to compile. if ba.ReadConsistency != roachpb.CONSISTENT { return nil, roachpb.NewErrorf("cannot use %s ReadConsistency in txn", ba.ReadConsistency) } lastIndex := len(ba.Requests) - 1 if lastIndex < 0 { return nil, nil } // firstWriteIndex is set to the index of the first command which is // a transactional write. If != -1, this indicates an intention to // write. This is in contrast to txn.Proto.Writing, which is set by // the coordinator when the first intent has been created, and which // lives for the life of the transaction. firstWriteIndex := -1 var firstWriteKey roachpb.Key for i, ru := range ba.Requests { args := ru.GetInner() if i < lastIndex { if _, ok := args.(*roachpb.EndTransactionRequest); ok { return nil, roachpb.NewErrorf("%s sent as non-terminal call", args.Method()) } } if roachpb.IsTransactionWrite(args) && firstWriteIndex == -1 { firstWriteKey = args.Header().Key firstWriteIndex = i } } haveTxnWrite := firstWriteIndex != -1 endTxnRequest, haveEndTxn := ba.Requests[lastIndex].GetInner().(*roachpb.EndTransactionRequest) needBeginTxn := !txn.Proto.Writing && haveTxnWrite needEndTxn := txn.Proto.Writing || haveTxnWrite elideEndTxn := haveEndTxn && !needEndTxn // If we're not yet writing in this txn, but intend to, insert a // begin transaction request before the first write command. if needBeginTxn { // If the transaction already has a key (we're in a restart), make // sure we set the key in the begin transaction request to the original. bt := &roachpb.BeginTransactionRequest{ Span: roachpb.Span{ Key: firstWriteKey, }, } if txn.Proto.Key != nil { bt.Key = txn.Proto.Key } // Inject the new request before position firstWriteIndex, taking // care to avoid unnecessary allocations. oldRequests := ba.Requests ba.Requests = make([]roachpb.RequestUnion, len(ba.Requests)+1) copy(ba.Requests, oldRequests[:firstWriteIndex]) ba.Requests[firstWriteIndex].MustSetInner(bt) copy(ba.Requests[firstWriteIndex+1:], oldRequests[firstWriteIndex:]) } if elideEndTxn { ba.Requests = ba.Requests[:lastIndex] } br, pErr := txn.sendInternal(ba) if elideEndTxn && pErr == nil { // Check that read only transactions do not violate their deadline. This can NOT // happen since the txn deadline is normally updated when it is about to expire // or expired. We will just keep the code for safety (see TestReacquireLeaseOnRestart). if endTxnRequest.Deadline != nil { if endTxnRequest.Deadline.Less(txn.Proto.Timestamp) { return nil, roachpb.NewErrorWithTxn(roachpb.NewTransactionAbortedError(), &txn.Proto) } } // This normally happens on the server and sent back in response // headers, but this transaction was optimized away. The caller may // still inspect the transaction struct, so we manually update it // here to emulate a true transaction. if endTxnRequest.Commit { txn.Proto.Status = roachpb.COMMITTED } else { txn.Proto.Status = roachpb.ABORTED } txn.finalized = true } // If we inserted a begin transaction request, remove it here. if needBeginTxn { if br != nil && br.Responses != nil { br.Responses = append(br.Responses[:firstWriteIndex], br.Responses[firstWriteIndex+1:]...) } // Handle case where inserted begin txn confused an indexed error. if pErr != nil && pErr.Index != nil { idx := pErr.Index.Index if idx == int32(firstWriteIndex) { // An error was encountered on begin txn; disallow the indexing. pErr.Index = nil } else if idx > int32(firstWriteIndex) { // An error was encountered after begin txn; decrement index. pErr.SetErrorIndex(idx - 1) } } } return br, pErr }