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

     1  // Copyright 2012-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"runtime/debug"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/juju/clock"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/featureflag"
    14  	"github.com/juju/loggo"
    15  	"github.com/juju/mgo/v3"
    16  	"github.com/juju/mgo/v3/txn"
    17  	jujutxn "github.com/juju/txn/v3"
    18  	"github.com/kr/pretty"
    19  
    20  	"github.com/juju/juju/controller"
    21  	"github.com/juju/juju/feature"
    22  	"github.com/juju/juju/mongo"
    23  )
    24  
    25  var txnLogger = loggo.GetLogger("juju.state.txn")
    26  
    27  type SessionCloser func()
    28  
    29  func dontCloseAnything() {}
    30  
    31  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/database_mock.go github.com/juju/juju/state Database
    32  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/mongo_mock.go github.com/juju/juju/mongo Collection,Query
    33  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/txn_mock.go github.com/juju/txn/v3 Runner
    34  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/clock_mock.go github.com/juju/clock Clock
    35  
    36  // Database exposes the mongodb capabilities that most of state should see.
    37  type Database interface {
    38  
    39  	// Copy returns a matching Database with its own session, and a
    40  	// func that must be called when the Database is no longer needed.
    41  	//
    42  	// GetCollection and TransactionRunner results from the resulting Database
    43  	// will all share a session; this does not absolve you of responsibility
    44  	// for calling those collections' closers.
    45  	Copy() (Database, SessionCloser)
    46  
    47  	// CopyForModel returns a matching Database with its own session and
    48  	// its own modelUUID and a func that must be called when the Database is no
    49  	// longer needed.
    50  	//
    51  	// Same warnings apply for CopyForModel than for Copy.
    52  	CopyForModel(modelUUID string) (Database, SessionCloser)
    53  
    54  	// GetCollection returns the named Collection, and a func that must be
    55  	// called when the Collection is no longer needed. The returned Collection
    56  	// might or might not have its own session, depending on the Database; the
    57  	// closer must always be called regardless.
    58  	//
    59  	// If the schema specifies model-filtering for the named collection,
    60  	// the returned collection will automatically filter queries; for details,
    61  	// see modelStateCollection.
    62  	GetCollection(name string) (mongo.Collection, SessionCloser)
    63  
    64  	// GetCollectionFor returns the named collection, scoped for the
    65  	// model specified. As for GetCollection, a closer is also returned.
    66  	GetCollectionFor(modelUUID, name string) (mongo.Collection, SessionCloser)
    67  
    68  	// GetRawCollection returns the named mgo Collection. As no
    69  	// automatic model filtering is performed by the returned
    70  	// collection it should be rarely used. GetCollection() should be
    71  	// used in almost all cases.
    72  	GetRawCollection(name string) (*mgo.Collection, SessionCloser)
    73  
    74  	// TransactionRunner returns a runner responsible for making changes to
    75  	// the database, and a func that must be called when the runner is no longer
    76  	// needed. The returned Runner might or might not have its own session,
    77  	// depending on the Database; the closer must always be called regardless.
    78  	//
    79  	// It will reject transactions that reference raw-access (or unknown)
    80  	// collections; it will automatically rewrite operations that reference
    81  	// non-global collections; and it will ensure that non-global documents can
    82  	// only be inserted while the corresponding model is still Alive.
    83  	TransactionRunner() (jujutxn.Runner, SessionCloser)
    84  
    85  	// RunTransaction is a convenience method for running a single
    86  	// transaction.
    87  	RunTransaction(ops []txn.Op) error
    88  
    89  	// RunTransactionFor is a convenience method for running a single
    90  	// transaction for the model specified.
    91  	RunTransactionFor(modelUUID string, ops []txn.Op) error
    92  
    93  	// RunRawTransaction is a convenience method that will run a
    94  	// single transaction using a "raw" transaction runner that won't
    95  	// perform model filtering.
    96  	RunRawTransaction(ops []txn.Op) error
    97  
    98  	// Run is a convenience method running a transaction using a
    99  	// transaction building function.
   100  	Run(transactions jujutxn.TransactionSource) error
   101  
   102  	// Run is a convenience method running a transaction using a
   103  	// transaction building function using a "raw" transaction runner
   104  	// that won't perform model filtering.
   105  	RunRaw(transactions jujutxn.TransactionSource) error
   106  
   107  	// Schema returns the schema used to load the database. The returned schema
   108  	// is not a copy and must not be modified.
   109  	Schema() CollectionSchema
   110  }
   111  
   112  // Change represents any mgo/txn-representable change to a Database.
   113  type Change interface {
   114  
   115  	// Prepare ensures that db is in a valid base state for applying
   116  	// the change, and returns mgo/txn operations that will fail any
   117  	// enclosing transaction if the state has materially changed; or
   118  	// returns an error.
   119  	Prepare(db Database) ([]txn.Op, error)
   120  }
   121  
   122  // ErrChangeComplete can be returned from Prepare to finish an Apply
   123  // attempt and report success without taking any further action.
   124  var ErrChangeComplete = errors.New("change complete")
   125  
   126  // Apply runs the supplied Change against the supplied Database. If it
   127  // returns no error, the change succeeded.
   128  func Apply(db Database, change Change) error {
   129  	db, dbCloser := db.Copy()
   130  	defer dbCloser()
   131  
   132  	buildTxn := func(int) ([]txn.Op, error) {
   133  		ops, err := change.Prepare(db)
   134  		if errors.Cause(err) == ErrChangeComplete {
   135  			return nil, jujutxn.ErrNoOperations
   136  		}
   137  		if err != nil {
   138  			return nil, errors.Trace(err)
   139  		}
   140  		return ops, nil
   141  	}
   142  
   143  	runner, tCloser := db.TransactionRunner()
   144  	defer tCloser()
   145  	if err := runner.Run(buildTxn); err != nil {
   146  		return errors.Trace(err)
   147  	}
   148  	return nil
   149  }
   150  
   151  // CollectionInfo describes important features of a collection.
   152  type CollectionInfo struct {
   153  
   154  	// explicitCreate, if non-nil, will cause the collection to be explicitly
   155  	// Create~d (with the given value) before ensuring indexes.
   156  	explicitCreate *mgo.CollectionInfo
   157  
   158  	// indexes listed here will be EnsureIndex~ed before state is opened.
   159  	indexes []mgo.Index
   160  
   161  	// global collections will not have model filtering applied. Non-
   162  	// global collections will have both transactions and reads filtered by
   163  	// relevant model uuid.
   164  	global bool
   165  
   166  	// rawAccess collections can be safely accessed as a mongo.WriteCollection.
   167  	// Direct database access to txn-aware collections is strongly discouraged:
   168  	// merely writing directly to a field makes it impossible to use that field
   169  	// with mgo/txn; in the worst case, document deletion can destroy critical
   170  	// parts of the state distributed by mgo/txn, causing different runners to
   171  	// choose different global transaction orderings; this can in turn cause
   172  	// operations to be skipped.
   173  	//
   174  	// Short explanation follows: two different runners pick different -- but
   175  	// overlapping -- "next" transactions; they each pick the same txn-id; the
   176  	// first runner writes to an overlapping document and records the txn-id;
   177  	// and then the second runner inspects that document, sees that the chosen
   178  	// txn-id has already been applied, and <splat> skips that operation.
   179  	//
   180  	// Goodbye consistency. So please don't mix txn and non-txn writes without
   181  	// very careful analysis; and then, please, just don't do it anyway. If you
   182  	// need raw mgo, use a rawAccess collection.
   183  	rawAccess bool
   184  }
   185  
   186  // CollectionSchema defines the set of collections used in juju.
   187  type CollectionSchema map[string]CollectionInfo
   188  
   189  // Create causes all recorded collections to be created and indexed as specified
   190  func (schema CollectionSchema) Create(
   191  	db *mgo.Database,
   192  	settings *controller.Config,
   193  ) error {
   194  	for name, info := range schema {
   195  		rawCollection := db.C(name)
   196  		if spec := info.explicitCreate; spec != nil {
   197  			if err := createCollection(rawCollection, spec); err != nil {
   198  				return mongo.MaybeUnauthorizedf(err, "cannot create collection %q", name)
   199  			}
   200  		} else {
   201  			// With server-side transactions, we need to create all the collections
   202  			// outside of a transaction (we don't want to create the collection
   203  			// as a side-effect.)
   204  			if err := createCollection(rawCollection, &mgo.CollectionInfo{}); err != nil {
   205  				return mongo.MaybeUnauthorizedf(err, "cannot create collection %q", name)
   206  			}
   207  		}
   208  		for _, index := range info.indexes {
   209  			if err := rawCollection.EnsureIndex(index); err != nil {
   210  				return mongo.MaybeUnauthorizedf(err, "cannot create index")
   211  			}
   212  		}
   213  	}
   214  	return nil
   215  }
   216  
   217  const codeNamespaceExists = 48
   218  
   219  // createCollection swallows collection-already-exists errors.
   220  func createCollection(raw *mgo.Collection, spec *mgo.CollectionInfo) error {
   221  	err := raw.Create(spec)
   222  	if err, ok := err.(*mgo.QueryError); ok {
   223  		// 48 is collection already exists.
   224  		if err.Code == codeNamespaceExists {
   225  			return nil
   226  		}
   227  	}
   228  	if mgoAlreadyExistsErr(err) {
   229  		return nil
   230  	}
   231  	return err
   232  }
   233  
   234  // database implements Database.
   235  type database struct {
   236  
   237  	// raw is the underlying mgo Database.
   238  	raw *mgo.Database
   239  
   240  	// schema specifies how the various collections must be handled.
   241  	schema CollectionSchema
   242  
   243  	// modelUUID is used to automatically filter queries and operations on
   244  	// certain collections (as defined in .schema).
   245  	modelUUID string
   246  
   247  	// runner exists for testing purposes; if non-nil, the result of
   248  	// TransactionRunner will always ultimately use this value to run
   249  	// all transactions. Setting it renders the database goroutine-unsafe.
   250  	runner jujutxn.Runner
   251  
   252  	// ownSession is used to avoid copying additional sessions in a database
   253  	// resulting from Copy.
   254  	ownSession bool
   255  
   256  	// runTransactionObserver is passed on to txn.TransactionRunner, to be
   257  	// invoked after calls to Run and RunTransaction.
   258  	runTransactionObserver RunTransactionObserverFunc
   259  
   260  	// clock is used to time how long transactions take to run
   261  	clock clock.Clock
   262  
   263  	// maxTxnAttempts is used when creating the txn runner to control how
   264  	// many attempts a txn should have.
   265  	maxTxnAttempts int
   266  
   267  	mu           sync.RWMutex
   268  	queryTracker *queryTracker
   269  }
   270  
   271  // RunTransactionObserverFunc is the type of a function to be called
   272  // after an mgo/txn transaction is run.
   273  type RunTransactionObserverFunc func(dbName, modelUUID string, attempt int, duration time.Duration, ops []txn.Op, err error)
   274  
   275  func (db *database) copySession(modelUUID string) (*database, SessionCloser) {
   276  	session := db.raw.Session.Copy()
   277  	return &database{
   278  		raw:            db.raw.With(session),
   279  		schema:         db.schema,
   280  		modelUUID:      modelUUID,
   281  		runner:         db.runner,
   282  		ownSession:     true,
   283  		clock:          db.clock,
   284  		maxTxnAttempts: db.maxTxnAttempts,
   285  	}, session.Close
   286  }
   287  
   288  func (db *database) setTracker(tracker *queryTracker) {
   289  	db.mu.Lock()
   290  	db.queryTracker = tracker
   291  	db.mu.Unlock()
   292  }
   293  
   294  // Copy is part of the Database interface.
   295  func (db *database) Copy() (Database, SessionCloser) {
   296  	return db.copySession(db.modelUUID)
   297  }
   298  
   299  // CopyForModel is part of the Database interface.
   300  func (db *database) CopyForModel(modelUUID string) (Database, SessionCloser) {
   301  	return db.copySession(modelUUID)
   302  }
   303  
   304  // GetCollection is part of the Database interface.
   305  func (db *database) GetCollection(name string) (collection mongo.Collection, closer SessionCloser) {
   306  	info, found := db.schema[name]
   307  	if !found {
   308  		logger.Errorf("using unknown collection %q", name)
   309  		if featureflag.Enabled(feature.DeveloperMode) {
   310  			logger.Errorf("from %s", string(debug.Stack()))
   311  		}
   312  	}
   313  
   314  	// Copy session if necessary.
   315  	if db.ownSession {
   316  		collection = mongo.WrapCollection(db.raw.C(name))
   317  		closer = dontCloseAnything
   318  	} else {
   319  		collection, closer = mongo.CollectionFromName(db.raw, name)
   320  	}
   321  
   322  	// Apply model filtering.
   323  	if !info.global {
   324  		db.mu.RLock()
   325  		collection = &modelStateCollection{
   326  			WriteCollection: collection.Writeable(),
   327  			modelUUID:       db.modelUUID,
   328  			queryTracker:    db.queryTracker,
   329  		}
   330  		db.mu.RUnlock()
   331  	}
   332  
   333  	// Prevent layer-breaking.
   334  	if !info.rawAccess {
   335  		// TODO(fwereade): it would be nice to tweak the mongo.Collection
   336  		// interface a bit to drop Writeable in this situation, but it's
   337  		// not convenient yet.
   338  	}
   339  	return collection, closer
   340  }
   341  
   342  // GetCollectionFor is part of the Database interface.
   343  func (db *database) GetCollectionFor(modelUUID, name string) (mongo.Collection, SessionCloser) {
   344  	newDb, dbcloser := db.CopyForModel(modelUUID)
   345  	collection, closer := newDb.GetCollection(name)
   346  	return collection, func() {
   347  		closer()
   348  		dbcloser()
   349  	}
   350  }
   351  
   352  // GetRawCollection is part of the Database interface.
   353  func (db *database) GetRawCollection(name string) (*mgo.Collection, SessionCloser) {
   354  	collection, closer := db.GetCollection(name)
   355  	return collection.Writeable().Underlying(), closer
   356  }
   357  
   358  // TransactionRunner is part of the Database interface.
   359  func (db *database) TransactionRunner() (runner jujutxn.Runner, closer SessionCloser) {
   360  	runner = db.runner
   361  	closer = dontCloseAnything
   362  	if runner == nil {
   363  		raw := db.raw
   364  		if !db.ownSession {
   365  			session := raw.Session.Copy()
   366  			raw = raw.With(session)
   367  			closer = session.Close
   368  		}
   369  		observer := func(t jujutxn.Transaction) {
   370  			if txnLogger.IsTraceEnabled() {
   371  				txnLogger.Tracef("ran transaction in %.3fs (retries: %d) %# v\nerr: %v",
   372  					t.Duration.Seconds(), t.Attempt, pretty.Formatter(t.Ops), t.Error)
   373  			}
   374  		}
   375  		if db.runTransactionObserver != nil {
   376  			observer = func(t jujutxn.Transaction) {
   377  				if txnLogger.IsTraceEnabled() {
   378  					txnLogger.Tracef("ran transaction in %.3fs (retries: %d) %# v\nerr: %v",
   379  						t.Duration.Seconds(), t.Attempt, pretty.Formatter(t.Ops), t.Error)
   380  				}
   381  				db.runTransactionObserver(
   382  					db.raw.Name, db.modelUUID,
   383  					t.Attempt,
   384  					t.Duration,
   385  					t.Ops, t.Error,
   386  				)
   387  			}
   388  		}
   389  		params := jujutxn.RunnerParams{
   390  			Database:                  raw,
   391  			RunTransactionObserver:    observer,
   392  			Clock:                     db.clock,
   393  			TransactionCollectionName: "txns",
   394  			ChangeLogName:             "-",
   395  			ServerSideTransactions:    true,
   396  			MaxRetryAttempts:          db.maxTxnAttempts,
   397  		}
   398  		runner = jujutxn.NewRunner(params)
   399  	}
   400  	return &multiModelRunner{
   401  		rawRunner: runner,
   402  		modelUUID: db.modelUUID,
   403  		schema:    db.schema,
   404  	}, closer
   405  }
   406  
   407  // RunTransaction is part of the Database interface.
   408  func (db *database) RunTransaction(ops []txn.Op) error {
   409  	runner, closer := db.TransactionRunner()
   410  	defer closer()
   411  	return runner.RunTransaction(&jujutxn.Transaction{Ops: ops})
   412  }
   413  
   414  // RunTransactionFor is part of the Database interface.
   415  func (db *database) RunTransactionFor(modelUUID string, ops []txn.Op) error {
   416  	newDB, dbcloser := db.CopyForModel(modelUUID)
   417  	defer dbcloser()
   418  	runner, closer := newDB.TransactionRunner()
   419  	defer closer()
   420  	return runner.RunTransaction(&jujutxn.Transaction{Ops: ops})
   421  }
   422  
   423  // RunRawTransaction is part of the Database interface.
   424  func (db *database) RunRawTransaction(ops []txn.Op) error {
   425  	runner, closer := db.TransactionRunner()
   426  	defer closer()
   427  	if multiRunner, ok := runner.(*multiModelRunner); ok {
   428  		runner = multiRunner.rawRunner
   429  	}
   430  	return runner.RunTransaction(&jujutxn.Transaction{Ops: ops})
   431  }
   432  
   433  // Run is part of the Database interface.
   434  func (db *database) Run(transactions jujutxn.TransactionSource) error {
   435  	runner, closer := db.TransactionRunner()
   436  	defer closer()
   437  	return runner.Run(transactions)
   438  }
   439  
   440  // RunRaw is part of the Database interface.
   441  func (db *database) RunRaw(transactions jujutxn.TransactionSource) error {
   442  	runner, closer := db.TransactionRunner()
   443  	defer closer()
   444  	if multiRunner, ok := runner.(*multiModelRunner); ok {
   445  		runner = multiRunner.rawRunner
   446  	}
   447  	return runner.Run(transactions)
   448  }
   449  
   450  // Schema is part of the Database interface.
   451  func (db *database) Schema() CollectionSchema {
   452  	return db.schema
   453  }