// TestTxnMultipleCoord checks that a coordinator uses the Writing flag to // enforce that only one coordinator can be used for transactional writes. func TestTxnMultipleCoord(t *testing.T) { defer leaktest.AfterTest(t)() s, sender := createTestDB(t) defer s.Stop() testCases := []struct { args roachpb.Request writing bool ok bool }{ {roachpb.NewGet(roachpb.Key("a")), true, false}, {roachpb.NewGet(roachpb.Key("a")), false, true}, {roachpb.NewPut(roachpb.Key("a"), roachpb.Value{}), false, false}, // transactional write before begin {roachpb.NewPut(roachpb.Key("a"), roachpb.Value{}), true, false}, // must have switched coordinators } for i, tc := range testCases { txn := roachpb.NewTransaction("test", roachpb.Key("a"), 1, enginepb.SERIALIZABLE, s.Clock.Now(), s.Clock.MaxOffset().Nanoseconds()) txn.Writing = tc.writing reply, pErr := client.SendWrappedWith(context.Background(), sender, roachpb.Header{ Txn: txn, }, tc.args) if pErr == nil != tc.ok { t.Errorf("%d: %T (writing=%t): success_expected=%t, but got: %v", i, tc.args, tc.writing, tc.ok, pErr) } if pErr != nil { continue } txn = reply.Header().Txn // The transaction should come back rw if it started rw or if we just // wrote. isWrite := roachpb.IsTransactionWrite(tc.args) if (tc.writing || isWrite) != txn.Writing { t.Errorf("%d: unexpected writing state: %s", i, txn) } if !isWrite { continue } // Abort for clean shutdown. if _, pErr := client.SendWrappedWith(context.Background(), sender, roachpb.Header{ Txn: txn, }, &roachpb.EndTransactionRequest{ Commit: false, }); pErr != nil { t.Fatal(pErr) } } }
// 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. // // No transactional writes are allowed unless preceded by a begin // transaction request within the same batch. The exception is if the // transaction is already in state txn.Writing=true. func (tc *TxnCoordSender) maybeBeginTxn(ba *roachpb.BatchRequest) error { if len(ba.Requests) == 0 { return errors.Errorf("empty batch with txn") } if ba.Txn.ID == nil { // Create transaction without a key. The key is set when a begin // transaction request is received. // The initial timestamp may be communicated by a higher layer. // If so, use that. Otherwise make up a new one. timestamp := ba.Txn.OrigTimestamp if timestamp == hlc.ZeroTimestamp { timestamp = tc.clock.Now() } newTxn := roachpb.NewTransaction(ba.Txn.Name, nil, ba.UserPriority, ba.Txn.Isolation, timestamp, 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 } // Check for a begin transaction to set txn key based on the key of // the first transactional write. Also enforce that no transactional // writes occur before a begin transaction. var haveBeginTxn bool for _, req := range ba.Requests { args := req.GetInner() if bt, ok := args.(*roachpb.BeginTransactionRequest); ok { if haveBeginTxn || ba.Txn.Writing { return errors.Errorf("begin transaction requested twice in the same transaction: %s", ba.Txn) } haveBeginTxn = true if ba.Txn.Key == nil { ba.Txn.Key = bt.Key } } if roachpb.IsTransactionWrite(args) && !haveBeginTxn && !ba.Txn.Writing { return errors.Errorf("transactional write before begin transaction") } } 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 }
// TestSender mocks out some of the txn coordinator sender's // functionality. It responds to PutRequests using testPutResp. func newTestSender( pre, post func(roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error), ) SenderFunc { txnID := uuid.MakeV4() return func(_ context.Context, ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) { if ba.UserPriority == 0 { ba.UserPriority = 1 } if ba.Txn != nil && ba.Txn.ID == nil { ba.Txn.Key = txnKey ba.Txn.ID = &txnID } var br *roachpb.BatchResponse var pErr *roachpb.Error if pre != nil { br, pErr = pre(ba) } else { br = ba.CreateReply() } if pErr != nil { return nil, pErr } var writing bool status := roachpb.PENDING for i, req := range ba.Requests { args := req.GetInner() if _, ok := args.(*roachpb.PutRequest); ok { testPutRespCopy := testPutResp union := &br.Responses[i] // avoid operating on copy union.MustSetInner(&testPutRespCopy) } if roachpb.IsTransactionWrite(args) { writing = true } } if args, ok := ba.GetArg(roachpb.EndTransaction); ok { et := args.(*roachpb.EndTransactionRequest) writing = true if et.Commit { status = roachpb.COMMITTED } else { status = roachpb.ABORTED } } if ba.Txn != nil { txnClone := ba.Txn.Clone() br.Txn = &txnClone if pErr == nil { br.Txn.Writing = writing br.Txn.Status = status } } if post != nil { br, pErr = post(ba) } return br, pErr } }