storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/disk-cache-utils.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2019 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "container/list" 21 "encoding/hex" 22 "errors" 23 "fmt" 24 "io" 25 "math" 26 "os" 27 "path" 28 "strconv" 29 "strings" 30 "time" 31 32 "storj.io/minio/cmd/crypto" 33 ) 34 35 // CacheStatusType - whether the request was served from cache. 36 type CacheStatusType string 37 38 const ( 39 // CacheHit - whether object was served from cache. 40 CacheHit CacheStatusType = "HIT" 41 42 // CacheMiss - object served from backend. 43 CacheMiss CacheStatusType = "MISS" 44 ) 45 46 func (c CacheStatusType) String() string { 47 if c != "" { 48 return string(c) 49 } 50 return string(CacheMiss) 51 } 52 53 type cacheControl struct { 54 expiry time.Time 55 maxAge int 56 sMaxAge int 57 minFresh int 58 maxStale int 59 noStore bool 60 onlyIfCached bool 61 noCache bool 62 } 63 64 func (c *cacheControl) isStale(modTime time.Time) bool { 65 if c == nil { 66 return false 67 } 68 // response will never be stale if only-if-cached is set 69 if c.onlyIfCached { 70 return false 71 } 72 // Cache-Control value no-store indicates never cache 73 if c.noStore { 74 return true 75 } 76 // Cache-Control value no-cache indicates cache entry needs to be revalidated before 77 // serving from cache 78 if c.noCache { 79 return true 80 } 81 now := time.Now() 82 83 if c.sMaxAge > 0 && c.sMaxAge < int(now.Sub(modTime).Seconds()) { 84 return true 85 } 86 if c.maxAge > 0 && c.maxAge < int(now.Sub(modTime).Seconds()) { 87 return true 88 } 89 90 if !c.expiry.Equal(time.Time{}) && c.expiry.Before(time.Now().Add(time.Duration(c.maxStale))) { 91 return true 92 } 93 94 if c.minFresh > 0 && c.minFresh <= int(now.Sub(modTime).Seconds()) { 95 return true 96 } 97 98 return false 99 } 100 101 // returns struct with cache-control settings from user metadata. 102 func cacheControlOpts(o ObjectInfo) *cacheControl { 103 c := cacheControl{} 104 m := o.UserDefined 105 if !o.Expires.Equal(timeSentinel) { 106 c.expiry = o.Expires 107 } 108 109 var headerVal string 110 for k, v := range m { 111 if strings.ToLower(k) == "cache-control" { 112 headerVal = v 113 } 114 115 } 116 if headerVal == "" { 117 return nil 118 } 119 headerVal = strings.ToLower(headerVal) 120 headerVal = strings.TrimSpace(headerVal) 121 122 vals := strings.Split(headerVal, ",") 123 for _, val := range vals { 124 val = strings.TrimSpace(val) 125 126 if val == "no-store" { 127 c.noStore = true 128 continue 129 } 130 if val == "only-if-cached" { 131 c.onlyIfCached = true 132 continue 133 } 134 if val == "no-cache" { 135 c.noCache = true 136 continue 137 } 138 p := strings.Split(val, "=") 139 140 if len(p) != 2 { 141 continue 142 } 143 if p[0] == "max-age" || 144 p[0] == "s-maxage" || 145 p[0] == "min-fresh" || 146 p[0] == "max-stale" { 147 i, err := strconv.Atoi(p[1]) 148 if err != nil { 149 return nil 150 } 151 if p[0] == "max-age" { 152 c.maxAge = i 153 } 154 if p[0] == "s-maxage" { 155 c.sMaxAge = i 156 } 157 if p[0] == "min-fresh" { 158 c.minFresh = i 159 } 160 if p[0] == "max-stale" { 161 c.maxStale = i 162 } 163 } 164 } 165 return &c 166 } 167 168 // backendDownError returns true if err is due to backend failure or faulty disk if in server mode 169 func backendDownError(err error) bool { 170 _, backendDown := err.(BackendDown) 171 return backendDown || IsErr(err, baseErrs...) 172 } 173 174 // IsCacheable returns if the object should be saved in the cache. 175 func (o ObjectInfo) IsCacheable() bool { 176 if globalCacheKMS != nil { 177 return true 178 } 179 _, ok := crypto.IsEncrypted(o.UserDefined) 180 return !ok 181 } 182 183 // reads file cached on disk from offset upto length 184 func readCacheFileStream(filePath string, offset, length int64) (io.ReadCloser, error) { 185 if filePath == "" || offset < 0 { 186 return nil, errInvalidArgument 187 } 188 if err := checkPathLength(filePath); err != nil { 189 return nil, err 190 } 191 192 fr, err := os.Open(filePath) 193 if err != nil { 194 return nil, osErrToFileErr(err) 195 } 196 // Stat to get the size of the file at path. 197 st, err := fr.Stat() 198 if err != nil { 199 err = osErrToFileErr(err) 200 return nil, err 201 } 202 203 if err = os.Chtimes(filePath, time.Now(), st.ModTime()); err != nil { 204 return nil, err 205 } 206 207 // Verify if its not a regular file, since subsequent Seek is undefined. 208 if !st.Mode().IsRegular() { 209 return nil, errIsNotRegular 210 } 211 212 if err = os.Chtimes(filePath, time.Now(), st.ModTime()); err != nil { 213 return nil, err 214 } 215 216 // Seek to the requested offset. 217 if offset > 0 { 218 _, err = fr.Seek(offset, io.SeekStart) 219 if err != nil { 220 return nil, err 221 } 222 } 223 return struct { 224 io.Reader 225 io.Closer 226 }{Reader: io.LimitReader(fr, length), Closer: fr}, nil 227 } 228 229 func isCacheEncrypted(meta map[string]string) bool { 230 _, ok := meta[SSECacheEncrypted] 231 return ok 232 } 233 234 // decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS 235 func decryptCacheObjectETag(info *ObjectInfo) error { 236 // Directories are never encrypted. 237 if info.IsDir { 238 return nil 239 } 240 encrypted := crypto.S3.IsEncrypted(info.UserDefined) && isCacheEncrypted(info.UserDefined) 241 242 switch { 243 case encrypted: 244 if globalCacheKMS == nil { 245 return errKMSNotConfigured 246 } 247 keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(info.UserDefined) 248 if err != nil { 249 return err 250 } 251 extKey, err := globalCacheKMS.DecryptKey(keyID, kmsKey, crypto.Context{info.Bucket: path.Join(info.Bucket, info.Name)}) 252 if err != nil { 253 return err 254 } 255 var objectKey crypto.ObjectKey 256 if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), info.Bucket, info.Name); err != nil { 257 return err 258 } 259 etagStr := tryDecryptETag(objectKey[:], info.ETag, false) 260 // backend ETag was hex encoded before encrypting, so hex decode to get actual ETag 261 etag, err := hex.DecodeString(etagStr) 262 if err != nil { 263 return err 264 } 265 info.ETag = string(etag) 266 return nil 267 } 268 269 return nil 270 } 271 272 func isMetadataSame(m1, m2 map[string]string) bool { 273 if m1 == nil && m2 == nil { 274 return true 275 } 276 if (m1 == nil && m2 != nil) || (m2 == nil && m1 != nil) { 277 return false 278 } 279 if len(m1) != len(m2) { 280 return false 281 } 282 for k1, v1 := range m1 { 283 if v2, ok := m2[k1]; !ok || (v1 != v2) { 284 return false 285 } 286 } 287 return true 288 } 289 290 type fileScorer struct { 291 saveBytes uint64 292 now int64 293 maxHits int 294 // 1/size for consistent score. 295 sizeMult float64 296 297 // queue is a linked list of files we want to delete. 298 // The list is kept sorted according to score, highest at top, lowest at bottom. 299 queue list.List 300 queuedBytes uint64 301 seenBytes uint64 302 } 303 304 type queuedFile struct { 305 name string 306 versionID string 307 size uint64 308 score float64 309 } 310 311 // newFileScorer allows to collect files to save a specific number of bytes. 312 // Each file is assigned a score based on its age, size and number of hits. 313 // A list of files is maintained 314 func newFileScorer(saveBytes uint64, now int64, maxHits int) (*fileScorer, error) { 315 if saveBytes == 0 { 316 return nil, errors.New("newFileScorer: saveBytes = 0") 317 } 318 if now < 0 { 319 return nil, errors.New("newFileScorer: now < 0") 320 } 321 if maxHits <= 0 { 322 return nil, errors.New("newFileScorer: maxHits <= 0") 323 } 324 f := fileScorer{saveBytes: saveBytes, maxHits: maxHits, now: now, sizeMult: 1 / float64(saveBytes)} 325 f.queue.Init() 326 return &f, nil 327 } 328 329 func (f *fileScorer) addFile(name string, accTime time.Time, size int64, hits int) { 330 f.addFileWithObjInfo(ObjectInfo{ 331 Name: name, 332 AccTime: accTime, 333 Size: size, 334 }, hits) 335 } 336 337 func (f *fileScorer) addFileWithObjInfo(objInfo ObjectInfo, hits int) { 338 // Calculate how much we want to delete this object. 339 file := queuedFile{ 340 name: objInfo.Name, 341 versionID: objInfo.VersionID, 342 size: uint64(objInfo.Size), 343 } 344 f.seenBytes += uint64(objInfo.Size) 345 346 var score float64 347 if objInfo.ModTime.IsZero() { 348 // Mod time is not available with disk cache use atime. 349 score = float64(f.now - objInfo.AccTime.Unix()) 350 } else { 351 // if not used mod time when mod time is available. 352 score = float64(f.now - objInfo.ModTime.Unix()) 353 } 354 355 // Size as fraction of how much we want to save, 0->1. 356 szWeight := math.Max(0, (math.Min(1, float64(file.size)*f.sizeMult))) 357 // 0 at f.maxHits, 1 at 0. 358 hitsWeight := (1.0 - math.Max(0, math.Min(1.0, float64(hits)/float64(f.maxHits)))) 359 file.score = score * (1 + 0.25*szWeight + 0.25*hitsWeight) 360 // If we still haven't saved enough, just add the file 361 if f.queuedBytes < f.saveBytes { 362 f.insertFile(file) 363 f.trimQueue() 364 return 365 } 366 // If we score less than the worst, don't insert. 367 worstE := f.queue.Back() 368 if worstE != nil && file.score < worstE.Value.(queuedFile).score { 369 return 370 } 371 f.insertFile(file) 372 f.trimQueue() 373 } 374 375 // adjustSaveBytes allows to adjust the number of bytes to save. 376 // This can be used to adjust the count on the fly. 377 // Returns true if there still is a need to delete files (n+saveBytes >0), 378 // false if no more bytes needs to be saved. 379 func (f *fileScorer) adjustSaveBytes(n int64) bool { 380 if int64(f.saveBytes)+n <= 0 { 381 f.saveBytes = 0 382 f.trimQueue() 383 return false 384 } 385 if n < 0 { 386 f.saveBytes -= ^uint64(n - 1) 387 } else { 388 f.saveBytes += uint64(n) 389 } 390 if f.saveBytes == 0 { 391 f.queue.Init() 392 f.saveBytes = 0 393 return false 394 } 395 if n < 0 { 396 f.trimQueue() 397 } 398 return true 399 } 400 401 // insertFile will insert a file into the list, sorted by its score. 402 func (f *fileScorer) insertFile(file queuedFile) { 403 e := f.queue.Front() 404 for e != nil { 405 v := e.Value.(queuedFile) 406 if v.score < file.score { 407 break 408 } 409 e = e.Next() 410 } 411 f.queuedBytes += file.size 412 // We reached the end. 413 if e == nil { 414 f.queue.PushBack(file) 415 return 416 } 417 f.queue.InsertBefore(file, e) 418 } 419 420 // trimQueue will trim the back of queue and still keep below wantSave. 421 func (f *fileScorer) trimQueue() { 422 for { 423 e := f.queue.Back() 424 if e == nil { 425 return 426 } 427 v := e.Value.(queuedFile) 428 if f.queuedBytes-v.size < f.saveBytes { 429 return 430 } 431 f.queue.Remove(e) 432 f.queuedBytes -= v.size 433 } 434 } 435 436 // fileObjInfos returns all queued file object infos 437 func (f *fileScorer) fileObjInfos() []ObjectInfo { 438 res := make([]ObjectInfo, 0, f.queue.Len()) 439 e := f.queue.Front() 440 for e != nil { 441 qfile := e.Value.(queuedFile) 442 res = append(res, ObjectInfo{ 443 Name: qfile.name, 444 Size: int64(qfile.size), 445 VersionID: qfile.versionID, 446 }) 447 e = e.Next() 448 } 449 return res 450 } 451 452 func (f *fileScorer) purgeFunc(p func(qfile queuedFile)) { 453 e := f.queue.Front() 454 for e != nil { 455 p(e.Value.(queuedFile)) 456 e = e.Next() 457 } 458 } 459 460 // fileNames returns all queued file names. 461 func (f *fileScorer) fileNames() []string { 462 res := make([]string, 0, f.queue.Len()) 463 e := f.queue.Front() 464 for e != nil { 465 res = append(res, e.Value.(queuedFile).name) 466 e = e.Next() 467 } 468 return res 469 } 470 471 func (f *fileScorer) reset() { 472 f.queue.Init() 473 f.queuedBytes = 0 474 } 475 476 func (f *fileScorer) queueString() string { 477 var res strings.Builder 478 e := f.queue.Front() 479 i := 0 480 for e != nil { 481 v := e.Value.(queuedFile) 482 if i > 0 { 483 res.WriteByte('\n') 484 } 485 res.WriteString(fmt.Sprintf("%03d: %s (score: %.3f, bytes: %d)", i, v.name, v.score, v.size)) 486 i++ 487 e = e.Next() 488 } 489 return res.String() 490 } 491 492 // bytesToClear() returns the number of bytes to clear to reach low watermark 493 // w.r.t quota given disk total and free space, quota in % allocated to cache 494 // and low watermark % w.r.t allowed quota. 495 // If the high watermark hasn't been reached 0 will be returned. 496 func bytesToClear(total, free int64, quotaPct, lowWatermark, highWatermark uint64) uint64 { 497 used := total - free 498 quotaAllowed := total * (int64)(quotaPct) / 100 499 highWMUsage := total * (int64)(highWatermark*quotaPct) / (100 * 100) 500 if used < highWMUsage { 501 return 0 502 } 503 // Return bytes needed to reach low watermark. 504 lowWMUsage := total * (int64)(lowWatermark*quotaPct) / (100 * 100) 505 return (uint64)(math.Min(float64(quotaAllowed), math.Max(0.0, float64(used-lowWMUsage)))) 506 }