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