github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/space/space_test.go (about)

     1  // Package space_test is a unit test for the package.
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package space_test
     6  
     7  import (
     8  	"crypto/rand"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/NVIDIA/aistore/api/apc"
    16  	"github.com/NVIDIA/aistore/cmn"
    17  	"github.com/NVIDIA/aistore/cmn/cos"
    18  	"github.com/NVIDIA/aistore/core"
    19  	"github.com/NVIDIA/aistore/core/meta"
    20  	"github.com/NVIDIA/aistore/core/mock"
    21  	"github.com/NVIDIA/aistore/fs"
    22  	"github.com/NVIDIA/aistore/hk"
    23  	"github.com/NVIDIA/aistore/space"
    24  	"github.com/NVIDIA/aistore/tools/trand"
    25  	"github.com/NVIDIA/aistore/xact/xreg"
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  )
    29  
    30  const (
    31  	initialDiskUsagePct  = 0.9
    32  	hwm                  = 80
    33  	lwm                  = 50
    34  	numberOfCreatedFiles = 45
    35  	fileSize             = 10 * cos.MiB
    36  	blockSize            = cos.KiB
    37  	basePath             = "/tmp/space-tests"
    38  	bucketName           = "space-bck"
    39  	bucketNameAnother    = bucketName + "-another"
    40  )
    41  
    42  type fileMetadata struct {
    43  	name string
    44  	size int64
    45  }
    46  
    47  var gT *testing.T
    48  
    49  func TestEvictCleanup(t *testing.T) {
    50  	xreg.Init()
    51  	hk.TestInit()
    52  	cos.InitShortID(0)
    53  
    54  	RegisterFailHandler(Fail)
    55  	gT = t
    56  	RunSpecs(t, t.Name())
    57  }
    58  
    59  var _ = Describe("space evict/cleanup tests", func() {
    60  	Describe("Run", func() {
    61  		var (
    62  			filesPath  string
    63  			fpAnother  string
    64  			bckAnother cmn.Bck
    65  		)
    66  
    67  		BeforeEach(func() {
    68  			initConfig()
    69  			createAndAddMountpath(basePath)
    70  			core.T = newTargetLRUMock()
    71  			availablePaths := fs.GetAvail()
    72  			bck := cmn.Bck{Name: bucketName, Provider: apc.AIS, Ns: cmn.NsGlobal}
    73  			bckAnother = cmn.Bck{Name: bucketNameAnother, Provider: apc.AIS, Ns: cmn.NsGlobal}
    74  			filesPath = availablePaths[basePath].MakePathCT(&bck, fs.ObjectType)
    75  			fpAnother = availablePaths[basePath].MakePathCT(&bckAnother, fs.ObjectType)
    76  			cos.CreateDir(filesPath)
    77  			cos.CreateDir(fpAnother)
    78  		})
    79  
    80  		AfterEach(func() {
    81  			os.RemoveAll(basePath)
    82  		})
    83  
    84  		Describe("evict files", func() {
    85  			var ini *space.IniLRU
    86  			BeforeEach(func() {
    87  				ini = newIniLRU()
    88  			})
    89  			It("should not fail when there are no files", func() {
    90  				space.RunLRU(ini)
    91  			})
    92  
    93  			It("should evict correct number of files", func() {
    94  				if testing.Short() {
    95  					Skip("skipping in short mode")
    96  				}
    97  				saveRandomFiles(filesPath, numberOfCreatedFiles)
    98  
    99  				space.RunLRU(ini)
   100  
   101  				files, err := os.ReadDir(filesPath)
   102  				Expect(err).NotTo(HaveOccurred())
   103  				numberOfFilesLeft := len(files)
   104  
   105  				// too few files evicted
   106  				Expect(float64(numberOfFilesLeft) / numberOfCreatedFiles * initialDiskUsagePct).To(BeNumerically("<=", 0.01*lwm))
   107  				// to many files evicted
   108  				Expect(float64(numberOfFilesLeft+1) / numberOfCreatedFiles * initialDiskUsagePct).To(BeNumerically(">", 0.01*lwm))
   109  			})
   110  
   111  			It("should evict the oldest files", func() {
   112  				const numberOfFiles = 6
   113  
   114  				ini.GetFSStats = getMockGetFSStats(numberOfFiles)
   115  
   116  				oldFiles := []fileMetadata{
   117  					{getRandomFileName(3), fileSize},
   118  					{getRandomFileName(4), fileSize},
   119  					{getRandomFileName(5), fileSize},
   120  				}
   121  				saveRandomFilesWithMetadata(filesPath, oldFiles)
   122  				time.Sleep(1 * time.Second)
   123  				saveRandomFiles(filesPath, 3)
   124  
   125  				space.RunLRU(ini)
   126  
   127  				files, err := os.ReadDir(filesPath)
   128  				Expect(err).NotTo(HaveOccurred())
   129  				Expect(len(files)).To(Equal(3))
   130  
   131  				oldFilesNames := namesFromFilesMetadatas(oldFiles)
   132  				for _, name := range files {
   133  					Expect(cos.StringInSlice(name.Name(), oldFilesNames)).To(BeFalse())
   134  				}
   135  			})
   136  
   137  			It("should evict files of different sizes", func() {
   138  				const totalSize = 32 * cos.MiB
   139  				if testing.Short() {
   140  					Skip("skipping in short mode")
   141  				}
   142  
   143  				ini.GetFSStats = func(string) (blocks, bavail uint64, bsize int64, err error) {
   144  					bsize = blockSize
   145  					btaken := uint64(totalSize / blockSize)
   146  					blocks = uint64(float64(btaken) / initialDiskUsagePct)
   147  					bavail = blocks - btaken
   148  					return
   149  				}
   150  
   151  				// files sum up to 32Mb
   152  				files := []fileMetadata{
   153  					{getRandomFileName(0), int64(4 * cos.MiB)},
   154  					{getRandomFileName(1), int64(16 * cos.MiB)},
   155  					{getRandomFileName(2), int64(4 * cos.MiB)},
   156  					{getRandomFileName(3), int64(8 * cos.MiB)},
   157  				}
   158  				saveRandomFilesWithMetadata(filesPath, files)
   159  
   160  				// To go under lwm (50%), LRU should evict the oldest files until <=50% reached
   161  				// Those files are a 4MB file and a 16MB file
   162  				space.RunLRU(ini)
   163  
   164  				filesLeft, err := os.ReadDir(filesPath)
   165  				Expect(len(filesLeft)).To(Equal(2))
   166  				Expect(err).NotTo(HaveOccurred())
   167  
   168  				correctFilenamesLeft := namesFromFilesMetadatas(files[2:])
   169  				for _, name := range filesLeft {
   170  					Expect(cos.StringInSlice(name.Name(), correctFilenamesLeft)).To(BeTrue())
   171  				}
   172  			})
   173  
   174  			It("should evict only files from requested bucket [ignores LRU prop]", func() {
   175  				if testing.Short() {
   176  					Skip("skipping in short mode")
   177  				}
   178  				saveRandomFiles(fpAnother, numberOfCreatedFiles)
   179  				saveRandomFiles(filesPath, numberOfCreatedFiles)
   180  
   181  				ini.Buckets = []cmn.Bck{bckAnother}
   182  				ini.Force = true // Ignore LRU enabled
   183  				space.RunLRU(ini)
   184  
   185  				files, err := os.ReadDir(filesPath)
   186  				Expect(err).NotTo(HaveOccurred())
   187  				filesAnother, err := os.ReadDir(fpAnother)
   188  				Expect(err).NotTo(HaveOccurred())
   189  
   190  				numFilesLeft := len(files)
   191  				numFilesLeftAnother := len(filesAnother)
   192  
   193  				// files not evicted from bucket
   194  				Expect(numFilesLeft).To(BeNumerically("==", numberOfCreatedFiles))
   195  
   196  				// too few files evicted
   197  				Expect(float64(numFilesLeftAnother) / numberOfCreatedFiles * initialDiskUsagePct).To(BeNumerically("<=", 0.01*lwm))
   198  				// to many files evicted
   199  				Expect(float64(numFilesLeftAnother+1) / numberOfCreatedFiles * initialDiskUsagePct).To(BeNumerically(">", 0.01*lwm))
   200  			})
   201  		})
   202  
   203  		Describe("not evict files", func() {
   204  			var ini *space.IniLRU
   205  			BeforeEach(func() {
   206  				ini = newIniLRU()
   207  			})
   208  			It("should do nothing when disk usage is below hwm", func() {
   209  				const numberOfFiles = 4
   210  				config := cmn.GCO.BeginUpdate()
   211  				config.Space.HighWM = 95
   212  				config.Space.LowWM = 40
   213  				cmn.GCO.CommitUpdate(config)
   214  
   215  				ini.GetFSStats = getMockGetFSStats(numberOfFiles)
   216  
   217  				saveRandomFiles(filesPath, numberOfFiles)
   218  
   219  				space.RunLRU(ini)
   220  
   221  				files, err := os.ReadDir(filesPath)
   222  				Expect(err).NotTo(HaveOccurred())
   223  				Expect(len(files)).To(Equal(numberOfFiles))
   224  			})
   225  
   226  			It("should do nothing if dontevict time was not reached", func() {
   227  				const numberOfFiles = 6
   228  				config := cmn.GCO.BeginUpdate()
   229  				config.LRU.DontEvictTime = cos.Duration(5 * time.Minute)
   230  				cmn.GCO.CommitUpdate(config)
   231  
   232  				ini.GetFSStats = getMockGetFSStats(numberOfFiles)
   233  
   234  				saveRandomFiles(filesPath, numberOfFiles)
   235  
   236  				space.RunLRU(ini)
   237  
   238  				files, err := os.ReadDir(filesPath)
   239  				Expect(err).NotTo(HaveOccurred())
   240  				Expect(len(files)).To(Equal(numberOfFiles))
   241  			})
   242  
   243  			It("should not evict if LRU disabled and force is false", func() {
   244  				saveRandomFiles(fpAnother, numberOfCreatedFiles)
   245  
   246  				ini.Buckets = []cmn.Bck{bckAnother} // bckAnother has LRU disabled
   247  				space.RunLRU(ini)
   248  
   249  				filesAnother, err := os.ReadDir(fpAnother)
   250  				Expect(err).NotTo(HaveOccurred())
   251  
   252  				numFilesLeft := len(filesAnother)
   253  				Expect(numFilesLeft).To(BeNumerically("==", numberOfCreatedFiles))
   254  			})
   255  		})
   256  
   257  		Describe("cleanup 'deleted'", func() {
   258  			var ini *space.IniCln
   259  			BeforeEach(func() {
   260  				ini = newInitStoreCln()
   261  			})
   262  			It("should remove all deleted items", func() {
   263  				var (
   264  					availablePaths = fs.GetAvail()
   265  					mi             = availablePaths[basePath]
   266  				)
   267  
   268  				saveRandomFiles(filesPath, 10)
   269  				Expect(filesPath).To(BeADirectory())
   270  
   271  				err := mi.MoveToDeleted(filesPath)
   272  				Expect(err).NotTo(HaveOccurred())
   273  				Expect(filesPath).NotTo(BeADirectory())
   274  
   275  				files, err := os.ReadDir(mi.DeletedRoot())
   276  				Expect(err).NotTo(HaveOccurred())
   277  				Expect(len(files)).To(Equal(1))
   278  
   279  				space.RunCleanup(ini)
   280  
   281  				files, err = os.ReadDir(mi.DeletedRoot())
   282  				Expect(err).NotTo(HaveOccurred())
   283  				Expect(len(files)).To(Equal(0))
   284  			})
   285  		})
   286  	})
   287  })
   288  
   289  //
   290  // test helpers & utilities
   291  //
   292  
   293  func namesFromFilesMetadatas(fileMetadata []fileMetadata) []string {
   294  	result := make([]string, len(fileMetadata))
   295  	for i, file := range fileMetadata {
   296  		result[i] = file.name
   297  	}
   298  	return result
   299  }
   300  
   301  func mockGetFSUsedPercentage(string) (usedPrecentage int64, _ bool) {
   302  	return int64(initialDiskUsagePct * 100), true
   303  }
   304  
   305  func getMockGetFSStats(currentFilesNum int) func(string) (uint64, uint64, int64, error) {
   306  	currDiskUsage := initialDiskUsagePct
   307  	return func(string) (blocks, bavail uint64, bsize int64, err error) {
   308  		bsize = blockSize
   309  		btaken := uint64(currentFilesNum * fileSize / blockSize)
   310  		blocks = uint64(float64(btaken) / currDiskUsage) // gives around currDiskUsage of virtual disk usage
   311  		bavail = blocks - btaken
   312  		return
   313  	}
   314  }
   315  
   316  func newTargetLRUMock() *mock.TargetMock {
   317  	// Bucket owner mock, required for LOM
   318  	var (
   319  		bmdMock = mock.NewBaseBownerMock(
   320  			meta.NewBck(
   321  				bucketName, apc.AIS, cmn.NsGlobal,
   322  				&cmn.Bprops{
   323  					Cksum:  cmn.CksumConf{Type: cos.ChecksumNone},
   324  					LRU:    cmn.LRUConf{Enabled: true},
   325  					Access: apc.AccessAll,
   326  					BID:    0xa7b8c1d2,
   327  				},
   328  			),
   329  			meta.NewBck(
   330  				bucketNameAnother, apc.AIS, cmn.NsGlobal,
   331  				&cmn.Bprops{
   332  					Cksum:  cmn.CksumConf{Type: cos.ChecksumNone},
   333  					LRU:    cmn.LRUConf{Enabled: false},
   334  					Access: apc.AccessAll,
   335  					BID:    0xf4e3d2c1,
   336  				},
   337  			),
   338  		)
   339  		tMock = mock.NewTarget(bmdMock)
   340  	)
   341  	return tMock
   342  }
   343  
   344  func newIniLRU() *space.IniLRU {
   345  	xlru := &space.XactLRU{}
   346  	xlru.InitBase(cos.GenUUID(), apc.ActLRU, nil)
   347  	return &space.IniLRU{
   348  		Xaction:             xlru,
   349  		Config:              cmn.GCO.Get(),
   350  		StatsT:              mock.NewStatsTracker(),
   351  		GetFSUsedPercentage: mockGetFSUsedPercentage,
   352  		GetFSStats:          getMockGetFSStats(numberOfCreatedFiles),
   353  	}
   354  }
   355  
   356  func newInitStoreCln() *space.IniCln {
   357  	xcln := &space.XactCln{}
   358  	xcln.InitBase(cos.GenUUID(), apc.ActStoreCleanup, nil)
   359  	return &space.IniCln{
   360  		Xaction: xcln,
   361  		Config:  cmn.GCO.Get(),
   362  		StatsT:  mock.NewStatsTracker(),
   363  	}
   364  }
   365  
   366  func initConfig() {
   367  	config := cmn.GCO.BeginUpdate()
   368  	config.LRU.DontEvictTime = 0
   369  	config.Space.HighWM = hwm
   370  	config.Space.LowWM = lwm
   371  	config.LRU.Enabled = true
   372  	config.Log.Level = "3"
   373  	cmn.GCO.CommitUpdate(config)
   374  }
   375  
   376  func createAndAddMountpath(path string) {
   377  	cos.CreateDir(path)
   378  	fs.TestNew(nil)
   379  	fs.Add(path, "daeID")
   380  
   381  	fs.CSM.Reg(fs.ObjectType, &fs.ObjectContentResolver{}, true)
   382  	fs.CSM.Reg(fs.WorkfileType, &fs.WorkfileContentResolver{}, true)
   383  }
   384  
   385  func getRandomFileName(fileCounter int) string {
   386  	return fmt.Sprintf("%v-%v.txt", trand.String(13), fileCounter)
   387  }
   388  
   389  func saveRandomFile(filename string, size int64) {
   390  	buff := make([]byte, size)
   391  	_, err := cos.SaveReader(filename, rand.Reader, buff, cos.ChecksumNone, size)
   392  	Expect(err).NotTo(HaveOccurred())
   393  	lom := &core.LOM{}
   394  	err = lom.InitFQN(filename, nil)
   395  	Expect(err).NotTo(HaveOccurred())
   396  	lom.SetSize(size)
   397  	lom.IncVersion()
   398  	lom.SetAtimeUnix(time.Now().UnixNano())
   399  	Expect(lom.Persist()).NotTo(HaveOccurred())
   400  }
   401  
   402  func saveRandomFilesWithMetadata(filesPath string, files []fileMetadata) {
   403  	for _, file := range files {
   404  		saveRandomFile(path.Join(filesPath, file.name), file.size)
   405  	}
   406  }
   407  
   408  // Saves random bytes to a file with random name.
   409  // timestamps and names are not increasing in the same manner
   410  func saveRandomFiles(filesPath string, filesNumber int) {
   411  	for i := range filesNumber {
   412  		saveRandomFile(path.Join(filesPath, getRandomFileName(i)), fileSize)
   413  	}
   414  }