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 }