github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/state/refcounts_ns.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"github.com/juju/errors"
     8  	"github.com/juju/juju/mongo"
     9  	"gopkg.in/mgo.v2"
    10  	"gopkg.in/mgo.v2/bson"
    11  	"gopkg.in/mgo.v2/txn"
    12  )
    13  
    14  // refcountDoc holds a reference count. Refcounts are important to juju
    15  // because mgo/txn offers no other mechanisms for safely coordinating
    16  // deletion of unreferenced documents.
    17  //
    18  // TODO(fwereade) 2016-08-11 lp:1612163
    19  //
    20  // There are several places that use ad-hoc refcounts (application
    21  // UnitCount and RelationCount; and model refs; and many many more)
    22  // which should (1) be using separate refcount docs instead of dumping
    23  // them in entity docs and (2) be using *this* refcount functionality
    24  // rather than building their own ad-hoc variants.
    25  type refcountDoc struct {
    26  
    27  	// The _id field should hold some globalKey to identify what's
    28  	// being referenced, but there's no reason to express it in this
    29  	// document directly.
    30  
    31  	// RefCount holds the reference count for whatever this doc is
    32  	// referencing.
    33  	RefCount int `bson:"refcount"`
    34  }
    35  
    36  var (
    37  	errRefcountChanged = errors.New("refcount changed")
    38  )
    39  
    40  // nsRefcounts exposes methods for safely manipulating reference count
    41  // documents. (You can also manipulate them unsafely via the Just*
    42  // methods that don't keep track of DB state.)
    43  var nsRefcounts = nsRefcounts_{}
    44  
    45  // nsRefcounts_ backs nsRefcounts.
    46  type nsRefcounts_ struct{}
    47  
    48  // LazyCreateOp returns a txn.Op that creates a refcount document; or
    49  // false if the document already exists.
    50  func (ns nsRefcounts_) LazyCreateOp(coll mongo.Collection, key string) (txn.Op, bool, error) {
    51  	if exists, err := ns.exists(coll, key); err != nil {
    52  		return txn.Op{}, false, errors.Trace(err)
    53  	} else if exists {
    54  		return txn.Op{}, false, nil
    55  	}
    56  	return ns.JustCreateOp(coll.Name(), key, 0), true, nil
    57  }
    58  
    59  // StrictCreateOp returns a txn.Op that creates a refcount document as
    60  // configured, or an error if the document already exists.
    61  func (ns nsRefcounts_) StrictCreateOp(coll mongo.Collection, key string, value int) (txn.Op, error) {
    62  	if exists, err := ns.exists(coll, key); err != nil {
    63  		return txn.Op{}, errors.Trace(err)
    64  	} else if exists {
    65  		return txn.Op{}, errors.New("refcount already exists")
    66  	}
    67  	return ns.JustCreateOp(coll.Name(), key, value), nil
    68  }
    69  
    70  // CreateOrIncrefOp returns a txn.Op that creates a refcount document as
    71  // configured with a specified value; or increments any such refcount doc
    72  // that already exists.
    73  func (ns nsRefcounts_) CreateOrIncRefOp(coll mongo.Collection, key string, n int) (txn.Op, error) {
    74  	if exists, err := ns.exists(coll, key); err != nil {
    75  		return txn.Op{}, errors.Trace(err)
    76  	} else if !exists {
    77  		return ns.JustCreateOp(coll.Name(), key, n), nil
    78  	}
    79  	return ns.JustIncRefOp(coll.Name(), key, n), nil
    80  }
    81  
    82  // StrictIncRefOp returns a txn.Op that increments the value of a
    83  // refcount doc, or returns an error if it does not exist.
    84  func (ns nsRefcounts_) StrictIncRefOp(coll mongo.Collection, key string, n int) (txn.Op, error) {
    85  	if exists, err := ns.exists(coll, key); err != nil {
    86  		return txn.Op{}, errors.Trace(err)
    87  	} else if !exists {
    88  		return txn.Op{}, errors.New("does not exist")
    89  	}
    90  	return ns.JustIncRefOp(coll.Name(), key, n), nil
    91  }
    92  
    93  // AliveDecRefOp returns a txn.Op that decrements the value of a
    94  // refcount doc, or an error if the doc does not exist or the count
    95  // would go below 0.
    96  func (ns nsRefcounts_) AliveDecRefOp(coll mongo.Collection, key string) (txn.Op, error) {
    97  	if refcount, err := ns.read(coll, key); err != nil {
    98  		return txn.Op{}, errors.Trace(err)
    99  	} else if refcount < 1 {
   100  		return txn.Op{}, errors.New("cannot decRef below 0")
   101  	}
   102  	return ns.justDecRefOp(coll.Name(), key, 0), nil
   103  }
   104  
   105  // DyingDecRefOp returns a txn.Op that decrements the value of a
   106  // refcount doc and deletes it if the count reaches 0; if the Op will
   107  // cause a delete, the bool result will be true. It will return an error
   108  // if the doc does not exist or the count would go below 0.
   109  func (ns nsRefcounts_) DyingDecRefOp(coll mongo.Collection, key string) (txn.Op, bool, error) {
   110  	refcount, err := ns.read(coll, key)
   111  	if err != nil {
   112  		return txn.Op{}, false, errors.Trace(err)
   113  	}
   114  	if refcount < 1 {
   115  		return txn.Op{}, false, errors.New("cannot decRef below 0")
   116  	} else if refcount > 1 {
   117  		return ns.justDecRefOp(coll.Name(), key, 1), false, nil
   118  	}
   119  	return ns.JustRemoveOp(coll.Name(), key, 1), true, nil
   120  }
   121  
   122  // RemoveOp returns a txn.Op that removes a refcount doc so long as its
   123  // refcount is the supplied value, or an error.
   124  func (ns nsRefcounts_) RemoveOp(coll mongo.Collection, key string, value int) (txn.Op, error) {
   125  	refcount, err := ns.read(coll, key)
   126  	if err != nil {
   127  		return txn.Op{}, errors.Trace(err)
   128  	}
   129  	if refcount != value {
   130  		return txn.Op{}, errRefcountChanged
   131  	}
   132  	return ns.JustRemoveOp(coll.Name(), key, value), nil
   133  }
   134  
   135  // CurrentOp returns the current reference count value, and a txn.Op that
   136  // asserts that the refcount has that value, or an error. If the refcount
   137  // doc does not exist, then the op will assert that the document does not
   138  // exist instead, and no error is returned.
   139  func (ns nsRefcounts_) CurrentOp(coll mongo.Collection, key string) (txn.Op, int, error) {
   140  	refcount, err := ns.read(coll, key)
   141  	if errors.IsNotFound(err) {
   142  		return txn.Op{
   143  			C:      coll.Name(),
   144  			Id:     key,
   145  			Assert: txn.DocMissing,
   146  		}, 0, nil
   147  	}
   148  	if err != nil {
   149  		return txn.Op{}, -1, errors.Trace(err)
   150  	}
   151  	return txn.Op{
   152  		C:      coll.Name(),
   153  		Id:     key,
   154  		Assert: bson.D{{"refcount", refcount}},
   155  	}, refcount, nil
   156  }
   157  
   158  // JustCreateOp returns a txn.Op that creates a refcount document as
   159  // configured, *without* checking database state for sanity first.
   160  // You should avoid using this method in most cases.
   161  func (nsRefcounts_) JustCreateOp(collName, key string, value int) txn.Op {
   162  	return txn.Op{
   163  		C:      collName,
   164  		Id:     key,
   165  		Assert: txn.DocMissing,
   166  		Insert: bson.D{{"refcount", value}},
   167  	}
   168  }
   169  
   170  // JustIncRefOp returns a txn.Op that increments a refcount document by
   171  // the specified amount, as configured, *without* checking database state
   172  // for sanity first. You should avoid using this method in most cases.
   173  func (nsRefcounts_) JustIncRefOp(collName, key string, n int) txn.Op {
   174  	return txn.Op{
   175  		C:      collName,
   176  		Id:     key,
   177  		Assert: txn.DocExists,
   178  		Update: bson.D{{"$inc", bson.D{{"refcount", n}}}},
   179  	}
   180  }
   181  
   182  // JustRemoveOp returns a txn.Op that deletes a refcount doc so long as
   183  // the refcount matches count. You should avoid using this method in
   184  // most cases.
   185  func (ns nsRefcounts_) JustRemoveOp(collName, key string, count int) txn.Op {
   186  	op := txn.Op{
   187  		C:      collName,
   188  		Id:     key,
   189  		Remove: true,
   190  	}
   191  	if count >= 0 {
   192  		op.Assert = bson.D{{"refcount", count}}
   193  	}
   194  	return op
   195  }
   196  
   197  // justDecRefOp returns a txn.Op that decrements a refcount document by
   198  // 1, as configured, allowing it to drop no lower than limit; which must
   199  // not be less than zero. It's unexported, meaningless though that may
   200  // be, to encourage clients to *really* not use it: too many ways to
   201  // mess it up if you're not precisely aware of the context.
   202  func (nsRefcounts_) justDecRefOp(collName, key string, limit int) txn.Op {
   203  	return txn.Op{
   204  		C:      collName,
   205  		Id:     key,
   206  		Assert: bson.D{{"refcount", bson.D{{"$gt", limit}}}},
   207  		Update: bson.D{{"$inc", bson.D{{"refcount", -1}}}},
   208  	}
   209  }
   210  
   211  // exists returns whether the identified refcount doc exists.
   212  func (nsRefcounts_) exists(coll mongo.Collection, key string) (bool, error) {
   213  	count, err := coll.FindId(key).Count()
   214  	if err != nil {
   215  		return false, errors.Trace(err)
   216  	}
   217  	return count != 0, nil
   218  }
   219  
   220  // read returns the value stored in the identified refcount doc.
   221  func (nsRefcounts_) read(coll mongo.Collection, key string) (int, error) {
   222  	var doc refcountDoc
   223  	if err := coll.FindId(key).One(&doc); err == mgo.ErrNotFound {
   224  		return 0, errors.NotFoundf("refcount %q", key)
   225  	} else if err != nil {
   226  		return 0, errors.Trace(err)
   227  	}
   228  	return doc.RefCount, nil
   229  }