github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/txns.go (about)

     1  // Copyright 2012-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"runtime/debug"
     8  	"strings"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/mgo/v3/bson"
    12  	"github.com/juju/mgo/v3/txn"
    13  	jujutxn "github.com/juju/txn/v3"
    14  )
    15  
    16  func readTxnRevno(db Database, collectionName string, id interface{}) (int64, error) {
    17  	collection, closer := db.GetCollection(collectionName)
    18  	defer closer()
    19  	query := collection.FindId(id).Select(bson.D{{"txn-revno", 1}})
    20  	var result struct {
    21  		TxnRevno int64 `bson:"txn-revno"`
    22  	}
    23  	err := query.One(&result)
    24  	return result.TxnRevno, errors.Trace(err)
    25  }
    26  
    27  func (st *State) runRawTransaction(ops []txn.Op) error {
    28  	return st.database.RunRawTransaction(ops)
    29  }
    30  
    31  type multiModelRunner struct {
    32  	rawRunner jujutxn.Runner
    33  	schema    CollectionSchema
    34  	modelUUID string
    35  }
    36  
    37  func shortStack() string {
    38  	var rv []string
    39  	for _, line := range strings.Split(string(debug.Stack()), "\n") {
    40  		if len(line) > 0 && line[0] == '\t' {
    41  			rv = append(rv, strings.Split(line[1:], " ")[0])
    42  		}
    43  	}
    44  	// We definitely have at least 3 objects in rv - debug.Stack(), this function and the one that called it.
    45  	return strings.Join(rv[3:], " ")
    46  }
    47  
    48  var seenShortStacks = make(map[string]bool)
    49  
    50  // RunTransaction is part of the jujutxn.Runner interface. Operations
    51  // that affect multi-model collections will be modified to
    52  // ensure correct interaction with these collections.
    53  func (r *multiModelRunner) RunTransaction(tx *jujutxn.Transaction) error {
    54  	if len(tx.Ops) == 0 {
    55  		stack := shortStack()
    56  		// It is a warning that should be reported to us, but we definitely
    57  		// don't want to clutter up logs so we'll only write it once.
    58  		if !seenShortStacks[stack] {
    59  			seenShortStacks[stack] = true
    60  			logger.Warningf("Running no-op transaction - called by %s", stack)
    61  		}
    62  	}
    63  	newOps, err := r.updateOps(tx.Ops)
    64  	if err != nil {
    65  		return errors.Trace(err)
    66  	}
    67  	tx.Ops = newOps
    68  	return r.rawRunner.RunTransaction(tx)
    69  }
    70  
    71  // Run is part of the jujutxn.Runner interface. Operations returned by
    72  // the given "transactions" function that affect multi-model
    73  // collections will be modified to ensure correct interaction with
    74  // these collections.
    75  func (r *multiModelRunner) Run(transactions jujutxn.TransactionSource) error {
    76  	return r.rawRunner.Run(func(attempt int) ([]txn.Op, error) {
    77  		ops, err := transactions(attempt)
    78  		if err != nil {
    79  			// Don't use Trace here as jujutxn doens't use juju/errors
    80  			// and won't deal correctly with some returned errors.
    81  			return nil, err
    82  		}
    83  		newOps, err := r.updateOps(ops)
    84  		if err != nil {
    85  			return nil, errors.Trace(err)
    86  		}
    87  		return newOps, nil
    88  	})
    89  }
    90  
    91  // ResumeTransactions is part of the jujutxn.Runner interface.
    92  func (r *multiModelRunner) ResumeTransactions() error {
    93  	return errors.NotImplemented
    94  }
    95  
    96  // MaybePruneTransactions is part of the jujutxn.Runner interface.
    97  func (r *multiModelRunner) MaybePruneTransactions(opts jujutxn.PruneOptions) error {
    98  	return errors.NotImplemented
    99  }
   100  
   101  // updateOps modifies the Insert and Update fields in a slice of
   102  // txn.Ops to ensure they are multi-model safe where
   103  // possible. The returned []txn.Op is a new copy of the input (with
   104  // changes).
   105  func (r *multiModelRunner) updateOps(ops []txn.Op) ([]txn.Op, error) {
   106  	var outOps []txn.Op
   107  	for _, op := range ops {
   108  		collInfo, found := r.schema[op.C]
   109  		if !found {
   110  			return nil, errors.Errorf("forbidden transaction: references unknown collection %q", op.C)
   111  		}
   112  		if collInfo.rawAccess {
   113  			return nil, errors.Errorf("forbidden transaction: references raw-access collection %q", op.C)
   114  		}
   115  		outOp := op
   116  		if !collInfo.global {
   117  			outOp.Id = ensureModelUUIDIfString(r.modelUUID, op.Id)
   118  			if op.Insert != nil {
   119  				newInsert, err := mungeDocForMultiModel(op.Insert, r.modelUUID, modelUUIDRequired)
   120  				if err != nil {
   121  					return nil, errors.Annotatef(err, "cannot insert into %q", op.C)
   122  				}
   123  				outOp.Insert = newInsert
   124  			}
   125  			if op.Update != nil {
   126  				newUpdate, err := r.mungeUpdate(op.Update)
   127  				if err != nil {
   128  					return nil, errors.Annotatef(err, "cannot update %q", op.C)
   129  				}
   130  				outOp.Update = newUpdate
   131  			}
   132  		}
   133  		outOps = append(outOps, outOp)
   134  	}
   135  	return outOps, nil
   136  }
   137  
   138  // mungeUpdate takes the value of an txn.Op Update field and modifies
   139  // it to be multi-model safe, returning the modified document.
   140  func (r *multiModelRunner) mungeUpdate(updateDoc interface{}) (interface{}, error) {
   141  	switch doc := updateDoc.(type) {
   142  	case bson.D:
   143  		return r.mungeBsonDUpdate(doc)
   144  	case bson.M:
   145  		return r.mungeBsonMUpdate(doc)
   146  	default:
   147  		return nil, errors.Errorf("don't know how to handle %T", updateDoc)
   148  	}
   149  }
   150  
   151  // mungeBsonDUpdate modifies a txn.Op's Update field values expressed
   152  // as a bson.D and attempts to make it multi-model safe.
   153  //
   154  // Currently, only $set operations are munged.
   155  func (r *multiModelRunner) mungeBsonDUpdate(updateDoc bson.D) (bson.D, error) {
   156  	outDoc := make(bson.D, 0, len(updateDoc))
   157  	for _, elem := range updateDoc {
   158  		if elem.Name == "$set" {
   159  			newSetDoc, err := mungeDocForMultiModel(elem.Value, r.modelUUID, 0)
   160  			if err != nil {
   161  				return nil, errors.Trace(err)
   162  			}
   163  			elem = bson.DocElem{elem.Name, newSetDoc}
   164  		}
   165  		outDoc = append(outDoc, elem)
   166  	}
   167  	return outDoc, nil
   168  }
   169  
   170  // mungeBsonMUpdate modifies a txn.Op's Update field values expressed
   171  // as a bson.M and attempts to make it multi-model safe.
   172  //
   173  // Currently, only $set operations are munged.
   174  func (r *multiModelRunner) mungeBsonMUpdate(updateDoc bson.M) (bson.M, error) {
   175  	outDoc := make(bson.M)
   176  	for name, elem := range updateDoc {
   177  		if name == "$set" {
   178  			var err error
   179  			elem, err = mungeDocForMultiModel(elem, r.modelUUID, 0)
   180  			if err != nil {
   181  				return nil, errors.Trace(err)
   182  			}
   183  		}
   184  		outDoc[name] = elem
   185  	}
   186  	return outDoc, nil
   187  }