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 }