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