github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "fmt" 8 9 "github.com/juju/errors" 10 jujutxn "github.com/juju/txn" 11 "gopkg.in/juju/names.v2" 12 "gopkg.in/mgo.v2" 13 "gopkg.in/mgo.v2/txn" 14 15 "github.com/juju/juju/mongo" 16 ) 17 18 type SessionCloser func() 19 20 func dontCloseAnything() {} 21 22 // Database exposes the mongodb capabilities that most of state should see. 23 type Database interface { 24 25 // Copy returns a matching Database with its own session, and a 26 // func that must be called when the Database is no longer needed. 27 // 28 // GetCollection and TransactionRunner results from the resulting Database 29 // will all share a session; this does not absolve you of responsibility 30 // for calling those collections' closers. 31 Copy() (Database, SessionCloser) 32 33 // CopyForModel returns a matching Database with its own session and 34 // its own modelUUID and a func that must be called when the Database is no 35 // longer needed. 36 // 37 // Same warnings apply for CopyForModel than for Copy. 38 CopyForModel(modelUUID string) (Database, SessionCloser) 39 40 // GetCollection returns the named Collection, and a func that must be 41 // called when the Collection is no longer needed. The returned Collection 42 // might or might not have its own session, depending on the Database; the 43 // closer must always be called regardless. 44 // 45 // If the schema specifies model-filtering for the named collection, 46 // the returned collection will automatically filter queries; for details, 47 // see modelStateCollection. 48 GetCollection(name string) (mongo.Collection, SessionCloser) 49 50 // TransactionRunner() returns a runner responsible for making changes to 51 // the database, and a func that must be called when the runner is no longer 52 // needed. The returned Runner might or might not have its own session, 53 // depending on the Database; the closer must always be called regardless. 54 // 55 // It will reject transactions that reference raw-access (or unknown) 56 // collections; it will automatically rewrite operations that reference 57 // non-global collections; and it will ensure that non-global documents can 58 // only be inserted while the corresponding model is still Alive. 59 TransactionRunner() (jujutxn.Runner, SessionCloser) 60 61 // Schema returns the schema used to load the database. The returned schema 62 // is not a copy and must not be modified. 63 Schema() collectionSchema 64 } 65 66 // Change represents any mgo/txn-representable change to a Database. 67 type Change interface { 68 69 // Prepare ensures that db is in a valid base state for applying 70 // the change, and returns mgo/txn operations that will fail any 71 // enclosing transaction if the state has materially changed; or 72 // returns an error. 73 Prepare(db Database) ([]txn.Op, error) 74 } 75 76 // ErrChangeComplete can be returned from Prepare to finish an Apply 77 // attempt and report success without taking any further action. 78 var ErrChangeComplete = errors.New("change complete") 79 80 // Apply runs the supplied Change against the supplied Database. If it 81 // returns no error, the change succeeded. 82 func Apply(db Database, change Change) error { 83 db, closer := db.Copy() 84 defer closer() 85 86 buildTxn := func(int) ([]txn.Op, error) { 87 ops, err := change.Prepare(db) 88 if errors.Cause(err) == ErrChangeComplete { 89 return nil, jujutxn.ErrNoOperations 90 } 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 return ops, nil 95 } 96 97 runner, closer := db.TransactionRunner() 98 defer closer() 99 if err := runner.Run(buildTxn); err != nil { 100 return errors.Trace(err) 101 } 102 return nil 103 } 104 105 // collectionInfo describes important features of a collection. 106 type collectionInfo struct { 107 108 // explicitCreate, if non-nil, will cause the collection to be explicitly 109 // Create~d (with the given value) before ensuring indexes. 110 explicitCreate *mgo.CollectionInfo 111 112 // indexes listed here will be EnsureIndex~ed before state is opened. 113 indexes []mgo.Index 114 115 // global collections will not have model filtering applied. Non- 116 // global collections will have both transactions and reads filtered by 117 // relevant model uuid. 118 global bool 119 120 // rawAccess collections can be safely accessed as a mongo.WriteCollection. 121 // Direct database access to txn-aware collections is strongly discouraged: 122 // merely writing directly to a field makes it impossible to use that field 123 // with mgo/txn; in the worst case, document deletion can destroy critical 124 // parts of the state distributed by mgo/txn, causing different runners to 125 // choose different global transaction orderings; this can in turn cause 126 // operations to be skipped. 127 // 128 // Short explanation follows: two different runners pick different -- but 129 // overlapping -- "next" transactions; they each pick the same txn-id; the 130 // first runner writes to an overlapping document and records the txn-id; 131 // and then the second runner inspects that document, sees that the chosen 132 // txn-id has already been applied, and <splat> skips that operation. 133 // 134 // Goodbye consistency. So please don't mix txn and non-txn writes without 135 // very careful analysis; and then, please, just don't do it anyway. If you 136 // need raw mgo, use a rawAccess collection. 137 rawAccess bool 138 } 139 140 // collectionSchema defines the set of collections used in juju. 141 type collectionSchema map[string]collectionInfo 142 143 // Load causes all recorded collections to be created and indexed as specified; 144 // the returned Database will filter queries and transactions according to the 145 // suppplied model UUID. 146 func (schema collectionSchema) Load(db *mgo.Database, modelUUID string) (Database, error) { 147 if !names.IsValidModel(modelUUID) { 148 return nil, errors.New("invalid model UUID") 149 } 150 for name, info := range schema { 151 rawCollection := db.C(name) 152 if spec := info.explicitCreate; spec != nil { 153 if err := createCollection(rawCollection, spec); err != nil { 154 message := fmt.Sprintf("cannot create collection %q", name) 155 return nil, maybeUnauthorized(err, message) 156 } 157 } 158 for _, index := range info.indexes { 159 if err := rawCollection.EnsureIndex(index); err != nil { 160 return nil, maybeUnauthorized(err, "cannot create index") 161 } 162 } 163 } 164 return &database{ 165 raw: db, 166 schema: schema, 167 modelUUID: modelUUID, 168 }, nil 169 } 170 171 // createCollection swallows collection-already-exists errors. 172 func createCollection(raw *mgo.Collection, spec *mgo.CollectionInfo) error { 173 err := raw.Create(spec) 174 // The lack of error code for this error was reported upstream: 175 // https://jira.mongodb.org/browse/SERVER-6992 176 if err == nil || err.Error() == "collection already exists" { 177 return nil 178 } 179 return err 180 } 181 182 // database implements Database. 183 type database struct { 184 185 // raw is the underlying mgo Database. 186 raw *mgo.Database 187 188 // schema specifies how the various collections must be handled. 189 schema collectionSchema 190 191 // modelUUID is used to automatically filter queries and operations on 192 // certain collections (as defined in .schema). 193 modelUUID string 194 195 // runner exists for testing purposes; if non-nil, the result of 196 // TransactionRunner will always ultimately use this value to run 197 // all transactions. Setting it renders the database goroutine-unsafe. 198 runner jujutxn.Runner 199 200 // ownSession is used to avoid copying additional sessions in a database 201 // resulting from Copy. 202 ownSession bool 203 } 204 205 func (db *database) copySession(modelUUID string) (*database, SessionCloser) { 206 session := db.raw.Session.Copy() 207 return &database{ 208 raw: db.raw.With(session), 209 schema: db.schema, 210 modelUUID: modelUUID, 211 runner: db.runner, 212 ownSession: true, 213 }, session.Close 214 215 } 216 217 // Copy is part of the Database interface. 218 func (db *database) Copy() (Database, SessionCloser) { 219 return db.copySession(db.modelUUID) 220 } 221 222 // CopyForModel is part of the Database interface. 223 func (db *database) CopyForModel(modelUUID string) (Database, SessionCloser) { 224 return db.copySession(modelUUID) 225 } 226 227 // GetCollection is part of the Database interface. 228 func (db *database) GetCollection(name string) (collection mongo.Collection, closer SessionCloser) { 229 info, found := db.schema[name] 230 if !found { 231 logger.Errorf("using unknown collection %q", name) 232 } 233 234 // Copy session if necessary. 235 if db.ownSession { 236 collection = mongo.WrapCollection(db.raw.C(name)) 237 closer = dontCloseAnything 238 } else { 239 collection, closer = mongo.CollectionFromName(db.raw, name) 240 } 241 242 // Apply model filtering. 243 if !info.global { 244 collection = &modelStateCollection{ 245 WriteCollection: collection.Writeable(), 246 modelUUID: db.modelUUID, 247 } 248 } 249 250 // Prevent layer-breaking. 251 if !info.rawAccess { 252 // TODO(fwereade): it would be nice to tweak the mongo.Collection 253 // interface a bit to drop Writeable in this situation, but it's 254 // not convenient yet. 255 } 256 return collection, closer 257 } 258 259 // TransactionRunner is part of the Database interface. 260 func (db *database) TransactionRunner() (runner jujutxn.Runner, closer SessionCloser) { 261 runner = db.runner 262 closer = dontCloseAnything 263 if runner == nil { 264 raw := db.raw 265 if !db.ownSession { 266 session := raw.Session.Copy() 267 raw = raw.With(session) 268 closer = session.Close 269 } 270 params := jujutxn.RunnerParams{Database: raw} 271 runner = jujutxn.NewRunner(params) 272 } 273 return &multiModelRunner{ 274 rawRunner: runner, 275 modelUUID: db.modelUUID, 276 schema: db.schema, 277 }, closer 278 } 279 280 // Schema is part of the Database interface. 281 func (db *database) Schema() collectionSchema { 282 return db.schema 283 }