github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/prune.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/dustin/go-humanize"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/mgo/v3"
    14  	"github.com/juju/mgo/v3/bson"
    15  
    16  	"github.com/juju/juju/mongo"
    17  )
    18  
    19  // pruneCollection removes collection entries until
    20  // only entries newer than <maxLogTime> remain and also ensures
    21  // that the collection is smaller than <maxLogsMB> after the
    22  // deletion.
    23  func pruneCollection(
    24  	stop <-chan struct{},
    25  	mb modelBackend, maxHistoryTime time.Duration, maxHistoryMB int,
    26  	coll *mgo.Collection, ageField string, filter bson.D,
    27  	timeUnit TimeUnit,
    28  ) error {
    29  	return pruneCollectionAndChildren(stop, mb, maxHistoryTime, maxHistoryMB, coll, nil, ageField, "", filter, 1, timeUnit)
    30  }
    31  
    32  // pruneCollectionAndChildren removes collection entries until
    33  // only entries newer than <maxLogTime> remain and also ensures
    34  // that the collection (or child collection if specified) is smaller
    35  // than <maxLogsMB> after the deletion.
    36  func pruneCollectionAndChildren(stop <-chan struct{}, mb modelBackend, maxHistoryTime time.Duration, maxHistoryMB int,
    37  	coll, childColl *mgo.Collection, ageField, parentRefField string,
    38  	filter bson.D, sizeFactor float64, timeUnit TimeUnit,
    39  ) error {
    40  	p := collectionPruner{
    41  		st:              mb,
    42  		coll:            coll,
    43  		childColl:       childColl,
    44  		parentRefField:  parentRefField,
    45  		childCountRatio: sizeFactor,
    46  		maxAge:          maxHistoryTime,
    47  		maxSize:         maxHistoryMB,
    48  		ageField:        ageField,
    49  		filter:          filter,
    50  		timeUnit:        timeUnit,
    51  	}
    52  	if err := p.validate(); err != nil {
    53  		return errors.Trace(err)
    54  	}
    55  	if err := p.pruneByAge(stop); err != nil {
    56  		return errors.Trace(err)
    57  	}
    58  	// First try pruning, excluding any items that
    59  	// have an age field that is not yet set.
    60  	// ie only prune completed items.
    61  	if err := p.pruneBySize(stop); err != nil {
    62  		return errors.Trace(err)
    63  	}
    64  	if ageField == "" {
    65  		return nil
    66  	}
    67  	// If needed, prune additional incomplete items to
    68  	// get under the size limit.
    69  	p.ageField = ""
    70  	return errors.Trace(p.pruneBySize(stop))
    71  }
    72  
    73  const historyPruneBatchSize = 1000
    74  const historyPruneProgressSeconds = 15
    75  
    76  type doneCheck func() (bool, error)
    77  
    78  type TimeUnit string
    79  
    80  const (
    81  	NanoSeconds TimeUnit = "nanoseconds"
    82  	GoTime      TimeUnit = "goTime"
    83  )
    84  
    85  type collectionPruner struct {
    86  	st     modelBackend
    87  	coll   *mgo.Collection
    88  	filter bson.D
    89  
    90  	// If specified, these fields define subordinate
    91  	// entries to delete in a related collection.
    92  	// The child records refer to the parents via
    93  	// the value of the parentRefField.
    94  	childColl       *mgo.Collection
    95  	parentRefField  string
    96  	childCountRatio float64 // ratio of child records to parent records.
    97  
    98  	maxAge  time.Duration
    99  	maxSize int
   100  
   101  	ageField string
   102  	timeUnit TimeUnit
   103  }
   104  
   105  func (p *collectionPruner) validate() error {
   106  	if p.maxSize < 0 {
   107  		return errors.NotValidf("non-positive max size")
   108  	}
   109  	if p.maxAge < 0 {
   110  		return errors.NotValidf("non-positive max age")
   111  	}
   112  	if p.maxSize == 0 && p.maxAge == 0 {
   113  		return errors.NewNotValid(nil, "backlog size and age constraints are both 0")
   114  	}
   115  	if p.childColl != nil && p.parentRefField == "" {
   116  		return errors.NewNotValid(nil, "missing parent reference field when a child collection is specified")
   117  	}
   118  	return nil
   119  }
   120  
   121  func (p *collectionPruner) pruneByAge(stop <-chan struct{}) error {
   122  	if p.maxAge == 0 {
   123  		return nil
   124  	}
   125  
   126  	t := p.st.clock().Now().Add(-p.maxAge)
   127  	var age interface{}
   128  	var notSet interface{}
   129  
   130  	if p.timeUnit == NanoSeconds {
   131  		age = t.UnixNano()
   132  		notSet = 0
   133  	} else {
   134  		age = t
   135  		notSet = time.Time{}
   136  	}
   137  
   138  	query := bson.D{
   139  		{"model-uuid", p.st.ModelUUID()},
   140  		{p.ageField, bson.M{"$gt": notSet, "$lt": age}},
   141  	}
   142  	query = append(query, p.filter...)
   143  	iter := p.coll.Find(query).Select(bson.M{"_id": 1}).Iter()
   144  	defer func() { _ = iter.Close() }()
   145  
   146  	modelName, err := p.st.modelName()
   147  	if err != nil {
   148  		return errors.Trace(err)
   149  	}
   150  	logTemplate := fmt.Sprintf("%s age pruning (%s): %%d rows deleted", p.coll.Name, modelName)
   151  	deleted, err := deleteInBatches(stop, p.coll, p.childColl, p.parentRefField, iter, logTemplate, loggo.INFO, noEarlyFinish)
   152  	if err != nil {
   153  		return errors.Trace(err)
   154  	}
   155  	if deleted > 0 {
   156  		logger.Debugf("%s age pruning (%s): %d rows deleted", p.coll.Name, modelName, deleted)
   157  	}
   158  	return errors.Trace(iter.Close())
   159  }
   160  
   161  func (*collectionPruner) toDeleteCalculator(coll *mgo.Collection, maxSizeMB int, countRatio float64) (int, error) {
   162  	collKB, err := getCollectionKB(coll)
   163  	if err != nil {
   164  		return 0, errors.Annotate(err, "retrieving collection size")
   165  	}
   166  	maxSizeKB := maxSizeMB * humanize.KiByte
   167  	if collKB <= maxSizeKB {
   168  		return 0, nil
   169  	}
   170  	count, err := coll.Count()
   171  	if err == mgo.ErrNotFound || count <= 0 {
   172  		return 0, nil
   173  	}
   174  	if err != nil {
   175  		return 0, errors.Annotatef(err, "counting %s records", coll.Name)
   176  	}
   177  	// For large numbers of items we are making an assumption that the size of
   178  	// items can be averaged to give a reasonable number of items to drop to
   179  	// reach the goal size.
   180  	sizePerItem := float64(collKB) / float64(count)
   181  	if sizePerItem == 0 {
   182  		return 0, errors.Errorf("unexpected result calculating %s entry size", coll.Name)
   183  	}
   184  	return int(float64(collKB-maxSizeKB) / (sizePerItem * countRatio)), nil
   185  }
   186  
   187  func (p *collectionPruner) pruneBySize(stop <-chan struct{}) error {
   188  	if !p.st.IsController() {
   189  		// Only prune by size in the controller. Otherwise we might
   190  		// find that multiple pruners are trying to delete the latest
   191  		// 1000 rows and end up with more deleted than we expect.
   192  		return nil
   193  	}
   194  	if p.maxSize == 0 {
   195  		return nil
   196  	}
   197  	var toDelete int
   198  	var err error
   199  	if p.childColl == nil {
   200  		// We are only operating on a single collection so calculate the number
   201  		// of items to delete based on the size of that collection.
   202  		toDelete, err = p.toDeleteCalculator(p.coll, p.maxSize, 1.0)
   203  	} else {
   204  		// We need to free up space in a child collection so calculate the number
   205  		// of parent items to delete based on the size of the child collection and
   206  		// the ratio of child items per parent item.
   207  		toDelete, err = p.toDeleteCalculator(p.childColl, p.maxSize, p.childCountRatio)
   208  	}
   209  	if err != nil {
   210  		return errors.Annotate(err, "calculating items to delete")
   211  	}
   212  	if toDelete <= 0 {
   213  		return nil
   214  	}
   215  
   216  	// If age field is set, add a filter which
   217  	// excludes those items where the age field
   218  	// is not set, ie only prune completed items.
   219  	var filter bson.D
   220  	if p.ageField != "" {
   221  		var notSet interface{}
   222  		if p.timeUnit == NanoSeconds {
   223  			notSet = 0
   224  		} else {
   225  			notSet = time.Time{}
   226  		}
   227  		filter = bson.D{
   228  			{p.ageField, bson.M{"$gt": notSet}},
   229  		}
   230  	}
   231  	filter = append(filter, p.filter...)
   232  	query := p.coll.Find(filter)
   233  	if p.ageField != "" {
   234  		query = query.Sort(p.ageField)
   235  	}
   236  	iter := query.Limit(toDelete).Select(bson.M{"_id": 1}).Iter()
   237  	defer func() { _ = iter.Close() }()
   238  
   239  	template := fmt.Sprintf("%s size pruning: deleted %%d of %d (estimated)", p.coll.Name, toDelete)
   240  	deleted, err := deleteInBatches(stop, p.coll, p.childColl, p.parentRefField, iter, template, loggo.INFO, func() (bool, error) {
   241  		// Check that we still need to delete more
   242  		collKB, err := getCollectionKB(p.coll)
   243  		if err != nil {
   244  			return false, errors.Annotatef(err, "retrieving %s collection size", p.coll.Name)
   245  		}
   246  		if collKB <= p.maxSize*humanize.KiByte {
   247  			return true, nil
   248  		}
   249  		return false, nil
   250  	})
   251  
   252  	if err != nil {
   253  		return errors.Trace(err)
   254  	}
   255  
   256  	logger.Infof("%s size pruning finished: %d rows deleted", p.coll.Name, deleted)
   257  	return errors.Trace(iter.Close())
   258  }
   259  
   260  func deleteInBatches(
   261  	stop <-chan struct{},
   262  	coll *mgo.Collection,
   263  	childColl *mgo.Collection,
   264  	childField string,
   265  	iter mongo.Iterator,
   266  	logTemplate string,
   267  	logLevel loggo.Level,
   268  	shouldStop doneCheck,
   269  ) (int, error) {
   270  	var doc bson.M
   271  	chunk := coll.Bulk()
   272  	chunkSize := 0
   273  
   274  	var childChunk *mgo.Bulk
   275  	if childColl != nil {
   276  		childChunk = childColl.Bulk()
   277  	}
   278  
   279  	lastUpdate := time.Now()
   280  	deleted := 0
   281  	for iter.Next(&doc) {
   282  		select {
   283  		case <-stop:
   284  			return deleted, nil
   285  		default:
   286  		}
   287  		parentId := doc["_id"]
   288  		chunk.Remove(bson.D{{"_id", parentId}})
   289  		chunkSize++
   290  		if childChunk != nil {
   291  			if idStr, ok := parentId.(string); ok {
   292  				_, localParentId, ok := splitDocID(idStr)
   293  				if ok {
   294  					childChunk.RemoveAll(bson.D{{childField, localParentId}})
   295  				}
   296  			}
   297  		}
   298  		if chunkSize == historyPruneBatchSize {
   299  			_, err := chunk.Run()
   300  			// NotFound indicates that records were already deleted.
   301  			if err != nil && err != mgo.ErrNotFound {
   302  				return 0, errors.Annotate(err, "removing batch")
   303  			}
   304  
   305  			deleted += chunkSize
   306  			chunk = coll.Bulk()
   307  			chunkSize = 0
   308  
   309  			if childChunk != nil {
   310  				_, err := childChunk.Run()
   311  				// NotFound indicates that records were already deleted.
   312  				if err != nil && err != mgo.ErrNotFound {
   313  					return 0, errors.Annotate(err, "removing child batch")
   314  				}
   315  				childChunk = childColl.Bulk()
   316  			}
   317  
   318  			// Check that we still need to delete more
   319  			done, err := shouldStop()
   320  			if err != nil {
   321  				return 0, errors.Annotate(err, "checking whether to stop")
   322  			}
   323  			if done {
   324  				return deleted, nil
   325  			}
   326  
   327  			now := time.Now()
   328  			if now.Sub(lastUpdate) >= historyPruneProgressSeconds*time.Second {
   329  				logger.Logf(logLevel, logTemplate, deleted)
   330  				lastUpdate = now
   331  			}
   332  		}
   333  	}
   334  	if err := iter.Close(); err != nil {
   335  		return 0, errors.Annotate(err, "closing iterator")
   336  	}
   337  
   338  	if chunkSize > 0 {
   339  		_, err := chunk.Run()
   340  		if err != nil && err != mgo.ErrNotFound {
   341  			return 0, errors.Annotate(err, "removing remainder")
   342  		}
   343  		if childChunk != nil {
   344  			_, err := childChunk.Run()
   345  			if err != nil && err != mgo.ErrNotFound {
   346  				return 0, errors.Annotate(err, "removing child remainder")
   347  			}
   348  		}
   349  	}
   350  
   351  	return deleted + chunkSize, nil
   352  }
   353  
   354  func noEarlyFinish() (bool, error) {
   355  	return false, nil
   356  }