// EndTransaction either commits or aborts (rolls back) an extant // transaction according to the args.Commit parameter. func (r *Range) EndTransaction(batch engine.Engine, ms *engine.MVCCStats, args *proto.EndTransactionRequest, reply *proto.EndTransactionResponse) { if args.Txn == nil { reply.SetGoError(util.Errorf("no transaction specified to EndTransaction")) return } key := keys.TransactionKey(args.Txn.Key, args.Txn.ID) // Fetch existing transaction if possible. existTxn := &proto.Transaction{} ok, err := engine.MVCCGetProto(batch, key, proto.ZeroTimestamp, true, nil, existTxn) if err != nil { reply.SetGoError(err) return } // If the transaction record already exists, verify that we can either // commit it or abort it (according to args.Commit), and also that the // Timestamp and Epoch have not suffered regression. if ok { // Use the persisted transaction record as final transaction. reply.Txn = gogoproto.Clone(existTxn).(*proto.Transaction) if existTxn.Status == proto.COMMITTED { reply.SetGoError(proto.NewTransactionStatusError(existTxn, "already committed")) return } else if existTxn.Status == proto.ABORTED { reply.SetGoError(proto.NewTransactionAbortedError(existTxn)) return } else if args.Txn.Epoch < existTxn.Epoch { reply.SetGoError(proto.NewTransactionStatusError(existTxn, fmt.Sprintf("epoch regression: %d", args.Txn.Epoch))) return } else if args.Txn.Epoch == existTxn.Epoch && existTxn.Timestamp.Less(args.Txn.OrigTimestamp) { // The transaction record can only ever be pushed forward, so it's an // error if somehow the transaction record has an earlier timestamp // than the original transaction timestamp. reply.SetGoError(proto.NewTransactionStatusError(existTxn, fmt.Sprintf("timestamp regression: %s", args.Txn.OrigTimestamp))) return } // Take max of requested epoch and existing epoch. The requester // may have incremented the epoch on retries. if reply.Txn.Epoch < args.Txn.Epoch { reply.Txn.Epoch = args.Txn.Epoch } // Take max of requested priority and existing priority. This isn't // terribly useful, but we do it for completeness. if reply.Txn.Priority < args.Txn.Priority { reply.Txn.Priority = args.Txn.Priority } } else { // The transaction doesn't exist yet on disk; use the supplied version. reply.Txn = gogoproto.Clone(args.Txn).(*proto.Transaction) } // Take max of requested timestamp and possibly "pushed" txn // record timestamp as the final commit timestamp. if reply.Txn.Timestamp.Less(args.Timestamp) { reply.Txn.Timestamp = args.Timestamp } // Set transaction status to COMMITTED or ABORTED as per the // args.Commit parameter. if args.Commit { // If the isolation level is SERIALIZABLE, return a transaction // retry error if the commit timestamp isn't equal to the txn // timestamp. if args.Txn.Isolation == proto.SERIALIZABLE && !reply.Txn.Timestamp.Equal(args.Txn.OrigTimestamp) { reply.SetGoError(proto.NewTransactionRetryError(reply.Txn)) return } reply.Txn.Status = proto.COMMITTED } else { reply.Txn.Status = proto.ABORTED } // Persist the transaction record with updated status (& possibly timestamp). if err := engine.MVCCPutProto(batch, ms, key, proto.ZeroTimestamp, nil, reply.Txn); err != nil { reply.SetGoError(err) return } // Run triggers if successfully committed. Any failures running // triggers will set an error and prevent the batch from committing. if ct := args.InternalCommitTrigger; ct != nil { // Resolve any explicit intents. for _, key := range ct.Intents { if log.V(1) { log.Infof("resolving intent at %s on end transaction [%s]", key, reply.Txn.Status) } if err := engine.MVCCResolveWriteIntent(batch, ms, key, reply.Txn.Timestamp, reply.Txn); err != nil { reply.SetGoError(err) return } reply.Resolved = append(reply.Resolved, key) } // Run appropriate trigger. if reply.Txn.Status == proto.COMMITTED { if ct.SplitTrigger != nil { *ms = engine.MVCCStats{} // clear stats, as split will recompute from scratch. reply.SetGoError(r.splitTrigger(batch, ct.SplitTrigger)) } else if ct.MergeTrigger != nil { *ms = engine.MVCCStats{} // clear stats, as merge will recompute from scratch. reply.SetGoError(r.mergeTrigger(batch, ct.MergeTrigger)) } else if ct.ChangeReplicasTrigger != nil { reply.SetGoError(r.changeReplicasTrigger(ct.ChangeReplicasTrigger)) } } } }
// EndTransaction either commits or aborts (rolls back) an extant // transaction according to the args.Commit parameter. func (r *Range) EndTransaction(args *proto.EndTransactionRequest, reply *proto.EndTransactionResponse) { // Create the actual key to the system-local transaction table. key := engine.MakeKey(engine.KeyLocalTransactionPrefix, args.Key) // Start with supplied transaction, then possibly load from txn record. reply.Txn = gogoproto.Clone(args.Txn).(*proto.Transaction) // Fetch existing transaction if possible. existTxn := &proto.Transaction{} ok, err := engine.GetProto(r.engine, key, existTxn) if err != nil { reply.SetGoError(err) return } // If the transaction record already exists, verify that we can either // commit it or abort it (according to args.Commit), and also that the // Timestamp and Epoch have not suffered regression. if ok { if existTxn.Status == proto.COMMITTED { reply.SetGoError(proto.NewTransactionStatusError(existTxn, "already committed")) return } else if existTxn.Status == proto.ABORTED { reply.SetGoError(proto.NewTransactionStatusError(existTxn, "already aborted")) return } else if args.Txn.Epoch < existTxn.Epoch { reply.SetGoError(proto.NewTransactionStatusError(existTxn, fmt.Sprintf("epoch regression: %d", args.Txn.Epoch))) return } else if existTxn.Timestamp.Less(args.Txn.Timestamp) { // The transaction record can only ever be pushed forward, so it's an // error if somehow the transaction record has an earlier timestamp // than the transaction timestamp. reply.SetGoError(proto.NewTransactionStatusError(existTxn, fmt.Sprintf("timestamp regression: %+v", args.Txn.Timestamp))) return } // Use the persisted transaction record as final transaction. gogoproto.Merge(reply.Txn, existTxn) } // Take max of requested timestamp and possibly "pushed" txn // record timestamp as the final commit timestamp. if reply.Txn.Timestamp.Less(args.Timestamp) { reply.Txn.Timestamp = args.Timestamp } // Set transaction status to COMMITTED or ABORTED as per the // args.Commit parameter. if args.Commit { // If the isolation level is SERIALIZABLE, return a transaction // retry error if the commit timestamp isn't equal to the txn // timestamp. if args.Txn.Isolation == proto.SERIALIZABLE && !reply.Txn.Timestamp.Equal(args.Txn.Timestamp) { reply.SetGoError(proto.NewTransactionRetryError(reply.Txn)) return } reply.Txn.Status = proto.COMMITTED } else { reply.Txn.Status = proto.ABORTED } // Persist the transaction record with updated status (& possibly timestmap). if err := engine.PutProto(r.engine, key, reply.Txn); err != nil { reply.SetGoError(err) return } }