| // Copyright 2022 The etcd Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package txn |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| |
| "go.uber.org/zap" |
| "google.golang.org/protobuf/proto" |
| |
| pb "go.etcd.io/etcd/api/v3/etcdserverpb" |
| "go.etcd.io/etcd/api/v3/mvccpb" |
| "go.etcd.io/etcd/pkg/v3/traceutil" |
| "go.etcd.io/etcd/server/v3/lease" |
| "go.etcd.io/etcd/server/v3/storage/mvcc" |
| ) |
| |
| func Txn(ctx context.Context, lg *zap.Logger, rt *pb.TxnRequest, txnModeWriteWithSharedBuffer bool, kv mvcc.KV, lessor lease.Lessor, skipRangeExecution bool) (txnResp *pb.TxnResponse, trace *traceutil.Trace, err error) { |
| ctx, trace = traceutil.EnsureTrace(ctx, lg, "transaction") |
| isWrite := !IsTxnReadonly(rt) |
| // When the transaction contains write operations, we use ReadTx instead of |
| // ConcurrentReadTx to avoid extra overhead of copying buffer. |
| var mode mvcc.ReadTxMode |
| if isWrite && txnModeWriteWithSharedBuffer /*a.s.Cfg.ServerFeatureGate.Enabled(features.TxnModeWriteWithSharedBuffer)*/ { |
| mode = mvcc.SharedBufReadTxMode |
| } else { |
| mode = mvcc.ConcurrentReadTxMode |
| } |
| txnRead := kv.Read(mode, trace) |
| var txnPath []bool |
| trace.StepWithFunction( |
| func() { |
| txnPath = compareToPath(txnRead, rt) |
| }, |
| "compare", |
| ) |
| if isWrite { |
| trace.AddField(traceutil.Field{Key: "read_only", Value: false}) |
| } |
| _, err = checkTxn(trace, txnRead, rt, lessor, txnPath) |
| if err != nil { |
| txnRead.End() |
| return nil, nil, err |
| } |
| trace.Step("check requests") |
| // When executing mutable txnWrite ops, etcd must hold the txnWrite lock so |
| // readers do not see any intermediate results. Since writes are |
| // serialized on the raft loop, the revision in the read view will |
| // be the revision of the write txnWrite. |
| var txnWrite mvcc.TxnWrite |
| if isWrite { |
| txnRead.End() |
| txnWrite = kv.Write(trace) |
| } else { |
| txnWrite = mvcc.NewReadOnlyTxnWrite(txnRead) |
| } |
| txnResp, err = txn(ctx, lg, txnWrite, rt, isWrite, txnPath, skipRangeExecution) |
| txnWrite.End() |
| |
| trace.AddField( |
| traceutil.Field{Key: "number_of_response", Value: len(txnResp.Responses)}, |
| traceutil.Field{Key: "response_revision", Value: txnResp.Header.Revision}, |
| ) |
| return txnResp, trace, err |
| } |
| |
| func txn(ctx context.Context, lg *zap.Logger, txnWrite mvcc.TxnWrite, rt *pb.TxnRequest, isWrite bool, txnPath []bool, skipRangeExecution bool) (*pb.TxnResponse, error) { |
| txnResp, _ := newTxnResp(rt, txnPath) |
| _, err := executeTxn(ctx, lg, txnWrite, rt, txnPath, txnResp, skipRangeExecution) |
| if err != nil { |
| if isWrite { |
| // CAUTION: When a txn performing write operations starts, we always expect it to be successful. |
| // If a write failure is seen we SHOULD NOT try to recover the server, but crash with a panic to make the failure explicit. |
| // Trying to silently recover (e.g by ignoring the failed txn or calling txn.End() early) poses serious risks: |
| // - violation of transaction atomicity if some write operations have been partially executed |
| // - data inconsistency across different etcd members if they applied the txn asymmetrically |
| lg.Panic("unexpected error during txn with writes", zap.Error(err)) |
| } else { |
| lg.Error("unexpected error during readonly txn", zap.Error(err)) |
| } |
| } |
| rev := txnWrite.Rev() |
| if len(txnWrite.Changes()) != 0 { |
| rev++ |
| } |
| txnResp.Header.Revision = rev |
| return txnResp, err |
| } |
| |
| // newTxnResp allocates a txn response for a txn request given a path. |
| func newTxnResp(rt *pb.TxnRequest, txnPath []bool) (txnResp *pb.TxnResponse, txnCount int) { |
| reqs := rt.Success |
| if !txnPath[0] { |
| reqs = rt.Failure |
| } |
| resps := make([]*pb.ResponseOp, len(reqs)) |
| txnResp = &pb.TxnResponse{ |
| Responses: resps, |
| Succeeded: txnPath[0], |
| Header: &pb.ResponseHeader{}, |
| } |
| for i, req := range reqs { |
| switch tv := req.Request.(type) { |
| case *pb.RequestOp_RequestRange: |
| resps[i] = &pb.ResponseOp{Response: &pb.ResponseOp_ResponseRange{}} |
| case *pb.RequestOp_RequestPut: |
| resps[i] = &pb.ResponseOp{Response: &pb.ResponseOp_ResponsePut{}} |
| case *pb.RequestOp_RequestDeleteRange: |
| resps[i] = &pb.ResponseOp{Response: &pb.ResponseOp_ResponseDeleteRange{}} |
| case *pb.RequestOp_RequestTxn: |
| resp, txns := newTxnResp(tv.RequestTxn, txnPath[1:]) |
| resps[i] = &pb.ResponseOp{Response: &pb.ResponseOp_ResponseTxn{ResponseTxn: resp}} |
| txnPath = txnPath[1+txns:] |
| txnCount += txns + 1 |
| default: |
| } |
| } |
| return txnResp, txnCount |
| } |
| |
| func executeTxn(ctx context.Context, lg *zap.Logger, txnWrite mvcc.TxnWrite, rt *pb.TxnRequest, txnPath []bool, tresp *pb.TxnResponse, skipRangeExecution bool) (txns int, err error) { |
| trace := traceutil.Get(ctx) |
| reqs := rt.Success |
| if !txnPath[0] { |
| reqs = rt.Failure |
| } |
| |
| for i, req := range reqs { |
| respi := tresp.Responses[i].Response |
| switch tv := req.Request.(type) { |
| case *pb.RequestOp_RequestRange: |
| trace.StartSubTrace( |
| traceutil.Field{Key: "req_type", Value: "range"}, |
| traceutil.Field{Key: "range_begin", Value: string(tv.RequestRange.Key)}, |
| traceutil.Field{Key: "range_end", Value: string(tv.RequestRange.RangeEnd)}) |
| var resp *pb.RangeResponse |
| if skipRangeExecution { |
| rev := txnWrite.Rev() |
| if len(txnWrite.Changes()) > 0 { |
| rev++ |
| } |
| resp = &pb.RangeResponse{ |
| Header: &pb.ResponseHeader{Revision: rev}, |
| } |
| } else { |
| resp, err = executeRange(ctx, lg, txnWrite, tv.RequestRange, true) |
| if err != nil { |
| return 0, fmt.Errorf("applyTxn: failed Range: %w", err) |
| } |
| } |
| respi.(*pb.ResponseOp_ResponseRange).ResponseRange = resp |
| trace.StopSubTrace() |
| case *pb.RequestOp_RequestPut: |
| trace.StartSubTrace( |
| traceutil.Field{Key: "req_type", Value: "put"}, |
| traceutil.Field{Key: "key", Value: string(tv.RequestPut.Key)}, |
| traceutil.Field{Key: "req_size", Value: proto.Size(tv.RequestPut)}) |
| prevKV, err := getPrevKV(trace, txnWrite, tv.RequestPut) |
| if err != nil { |
| return 0, fmt.Errorf("applyTxn: failed to get prevKV on put: %w", err) |
| } |
| resp := put(ctx, txnWrite, tv.RequestPut, prevKV) |
| respi.(*pb.ResponseOp_ResponsePut).ResponsePut = resp |
| trace.StopSubTrace() |
| case *pb.RequestOp_RequestDeleteRange: |
| resp, err := deleteRange(ctx, txnWrite, tv.RequestDeleteRange) |
| if err != nil { |
| return 0, fmt.Errorf("applyTxn: failed DeleteRange: %w", err) |
| } |
| respi.(*pb.ResponseOp_ResponseDeleteRange).ResponseDeleteRange = resp |
| case *pb.RequestOp_RequestTxn: |
| resp := respi.(*pb.ResponseOp_ResponseTxn).ResponseTxn |
| applyTxns, err := executeTxn(ctx, lg, txnWrite, tv.RequestTxn, txnPath[1:], resp, skipRangeExecution) |
| if err != nil { |
| // don't wrap the error. It's a recursive call and err should be already wrapped |
| return 0, err |
| } |
| txns += applyTxns + 1 |
| txnPath = txnPath[applyTxns+1:] |
| default: |
| // empty union |
| } |
| } |
| return txns, nil |
| } |
| |
| func checkTxn(trace *traceutil.Trace, rv mvcc.ReadView, rt *pb.TxnRequest, lessor lease.Lessor, txnPath []bool) (int, error) { |
| txnCount := 0 |
| reqs := rt.Success |
| if !txnPath[0] { |
| reqs = rt.Failure |
| } |
| for _, req := range reqs { |
| var err error |
| var txns int |
| switch tv := req.Request.(type) { |
| case *pb.RequestOp_RequestRange: |
| err = checkRange(rv, tv.RequestRange) |
| case *pb.RequestOp_RequestPut: |
| err = checkPut(trace, rv, lessor, tv.RequestPut) |
| case *pb.RequestOp_RequestDeleteRange: |
| case *pb.RequestOp_RequestTxn: |
| txns, err = checkTxn(trace, rv, tv.RequestTxn, lessor, txnPath[1:]) |
| txnCount += txns + 1 |
| txnPath = txnPath[txns+1:] |
| default: |
| // empty union |
| } |
| if err != nil { |
| return 0, err |
| } |
| } |
| return txnCount, nil |
| } |
| |
| func compareInt64(a, b int64) int { |
| switch { |
| case a < b: |
| return -1 |
| case a > b: |
| return 1 |
| default: |
| return 0 |
| } |
| } |
| |
| func compareToPath(rv mvcc.ReadView, rt *pb.TxnRequest) []bool { |
| txnPath := make([]bool, 1) |
| ops := rt.Success |
| if txnPath[0] = applyCompares(rv, rt.Compare); !txnPath[0] { |
| ops = rt.Failure |
| } |
| for _, op := range ops { |
| tv, ok := op.Request.(*pb.RequestOp_RequestTxn) |
| if !ok || tv.RequestTxn == nil { |
| continue |
| } |
| txnPath = append(txnPath, compareToPath(rv, tv.RequestTxn)...) |
| } |
| return txnPath |
| } |
| |
| func applyCompares(rv mvcc.ReadView, cmps []*pb.Compare) bool { |
| for _, c := range cmps { |
| if !applyCompare(rv, c) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // applyCompare applies the compare request. |
| // If the comparison succeeds, it returns true. Otherwise, returns false. |
| func applyCompare(rv mvcc.ReadView, c *pb.Compare) bool { |
| // TODO: possible optimizations |
| // * chunk reads for large ranges to conserve memory |
| // * rewrite rules for common patterns: |
| // ex. "[a, b) createrev > 0" => "limit 1 /\ kvs > 0" |
| // * caching |
| rr, err := rv.Range(context.TODO(), c.Key, mkGteRange(c.RangeEnd), mvcc.RangeOptions{}) |
| if err != nil { |
| return false |
| } |
| if len(rr.KVs) == 0 { |
| if c.Target == pb.Compare_VALUE { |
| // Always fail if comparing a value on a key/keys that doesn't exist; |
| // nil == empty string in grpc; no way to represent missing value |
| return false |
| } |
| return compareKV(c, &mvccpb.KeyValue{}) |
| } |
| for _, kv := range rr.KVs { |
| if !compareKV(c, kv) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func compareKV(c *pb.Compare, ckv *mvccpb.KeyValue) bool { |
| var result int |
| rev := int64(0) |
| switch c.Target { |
| case pb.Compare_VALUE: |
| var v []byte |
| if tv, _ := c.TargetUnion.(*pb.Compare_Value); tv != nil { |
| v = tv.Value |
| } |
| result = bytes.Compare(ckv.Value, v) |
| case pb.Compare_CREATE: |
| if tv, _ := c.TargetUnion.(*pb.Compare_CreateRevision); tv != nil { |
| rev = tv.CreateRevision |
| } |
| result = compareInt64(ckv.CreateRevision, rev) |
| case pb.Compare_MOD: |
| if tv, _ := c.TargetUnion.(*pb.Compare_ModRevision); tv != nil { |
| rev = tv.ModRevision |
| } |
| result = compareInt64(ckv.ModRevision, rev) |
| case pb.Compare_VERSION: |
| if tv, _ := c.TargetUnion.(*pb.Compare_Version); tv != nil { |
| rev = tv.Version |
| } |
| result = compareInt64(ckv.Version, rev) |
| case pb.Compare_LEASE: |
| if tv, _ := c.TargetUnion.(*pb.Compare_Lease); tv != nil { |
| rev = tv.Lease |
| } |
| result = compareInt64(ckv.Lease, rev) |
| } |
| switch c.Result { |
| case pb.Compare_EQUAL: |
| return result == 0 |
| case pb.Compare_NOT_EQUAL: |
| return result != 0 |
| case pb.Compare_GREATER: |
| return result > 0 |
| case pb.Compare_LESS: |
| return result < 0 |
| } |
| return true |
| } |
| |
| func IsTxnSerializable(r *pb.TxnRequest) bool { |
| for _, u := range r.Success { |
| if r := u.GetRequestRange(); r == nil || !r.Serializable { |
| return false |
| } |
| } |
| for _, u := range r.Failure { |
| if r := u.GetRequestRange(); r == nil || !r.Serializable { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func IsTxnReadonly(r *pb.TxnRequest) bool { |
| for _, u := range r.Success { |
| if r := u.GetRequestRange(); r == nil { |
| return false |
| } |
| } |
| for _, u := range r.Failure { |
| if r := u.GetRequestRange(); r == nil { |
| return false |
| } |
| } |
| return true |
| } |