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  }