github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/fs/mpather/jogger_test.go (about)

     1  // Package mpather provides per-mountpath concepts.
     2  /*
     3   * Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package mpather_test
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"math/rand"
    12  	"os"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/NVIDIA/aistore/cmn"
    18  	"github.com/NVIDIA/aistore/cmn/atomic"
    19  	"github.com/NVIDIA/aistore/cmn/cos"
    20  	"github.com/NVIDIA/aistore/core"
    21  	"github.com/NVIDIA/aistore/fs"
    22  	"github.com/NVIDIA/aistore/fs/mpather"
    23  	"github.com/NVIDIA/aistore/memsys"
    24  	"github.com/NVIDIA/aistore/tools"
    25  	"github.com/NVIDIA/aistore/tools/tassert"
    26  )
    27  
    28  func TestJoggerGroup(t *testing.T) {
    29  	var (
    30  		desc = tools.ObjectsDesc{
    31  			CTs: []tools.ContentTypeDesc{
    32  				{Type: fs.WorkfileType, ContentCnt: 10},
    33  				{Type: fs.ObjectType, ContentCnt: 500},
    34  			},
    35  			MountpathsCnt: 10,
    36  			ObjectSize:    cos.KiB,
    37  		}
    38  		out     = tools.PrepareObjects(t, desc)
    39  		counter = atomic.NewInt32(0)
    40  	)
    41  	defer os.RemoveAll(out.Dir)
    42  
    43  	opts := &mpather.JgroupOpts{
    44  		Bck: out.Bck,
    45  		CTs: []string{fs.ObjectType},
    46  		VisitObj: func(_ *core.LOM, buf []byte) error {
    47  			tassert.Errorf(t, len(buf) == 0, "buffer expected to be empty")
    48  			counter.Inc()
    49  			return nil
    50  		},
    51  	}
    52  	jg := mpather.NewJoggerGroup(opts, cmn.GCO.Get(), "")
    53  	jg.Run()
    54  	<-jg.ListenFinished()
    55  
    56  	tassert.Errorf(
    57  		t, int(counter.Load()) == len(out.FQNs[fs.ObjectType]),
    58  		"invalid number of objects visited (%d vs %d)", counter.Load(), len(out.FQNs[fs.ObjectType]),
    59  	)
    60  
    61  	err := jg.Stop()
    62  	tassert.CheckFatal(t, err)
    63  }
    64  
    65  func TestJoggerGroupParallel(t *testing.T) {
    66  	var (
    67  		parallelOptions = []int{2, 8, 24}
    68  		objectsCnt      = 1000
    69  		mpathsCnt       = 3
    70  
    71  		desc = tools.ObjectsDesc{
    72  			CTs: []tools.ContentTypeDesc{
    73  				{Type: fs.ObjectType, ContentCnt: objectsCnt},
    74  			},
    75  			MountpathsCnt: mpathsCnt,
    76  			ObjectSize:    cos.KiB,
    77  		}
    78  		out     = tools.PrepareObjects(t, desc)
    79  		counter *atomic.Int32
    80  
    81  		mmsa = memsys.PageMM()
    82  	)
    83  	defer os.RemoveAll(out.Dir)
    84  
    85  	slab, err := mmsa.GetSlab(memsys.PageSize)
    86  	tassert.CheckFatal(t, err)
    87  
    88  	baseJgOpts := &mpather.JgroupOpts{
    89  		Bck:  out.Bck,
    90  		CTs:  []string{fs.ObjectType},
    91  		Slab: slab,
    92  		VisitObj: func(lom *core.LOM, buf []byte) error {
    93  			b := bytes.NewBuffer(buf[:0])
    94  			_, err = b.WriteString(lom.FQN)
    95  			tassert.CheckFatal(t, err)
    96  
    97  			if rand.Intn(objectsCnt/mpathsCnt)%20 == 0 {
    98  				// Sometimes sleep a while, to check if in this time some other goroutine does not populate the buffer.
    99  				time.Sleep(10 * time.Millisecond)
   100  			}
   101  			fqn := b.String()
   102  			// Checks that there are no concurrent writes on the same buffer.
   103  			tassert.Errorf(t, fqn == lom.FQN, "expected the correct FQN %q to be read, got %q", fqn, b.String())
   104  			counter.Inc()
   105  			return nil
   106  		},
   107  	}
   108  
   109  	for _, baseJgOpts.Parallel = range parallelOptions {
   110  		t.Run(fmt.Sprintf("TestJoggerGroupParallel/%d", baseJgOpts.Parallel), func(t *testing.T) {
   111  			counter = atomic.NewInt32(0)
   112  			jg := mpather.NewJoggerGroup(baseJgOpts, cmn.GCO.Get(), "")
   113  			jg.Run()
   114  			<-jg.ListenFinished()
   115  
   116  			tassert.Errorf(
   117  				t, int(counter.Load()) == len(out.FQNs[fs.ObjectType]),
   118  				"invalid number of objects visited (%d vs %d)", counter.Load(), len(out.FQNs[fs.ObjectType]),
   119  			)
   120  
   121  			err := jg.Stop()
   122  			tassert.CheckFatal(t, err)
   123  		})
   124  	}
   125  }
   126  
   127  func TestJoggerGroupLoad(t *testing.T) {
   128  	var (
   129  		desc = tools.ObjectsDesc{
   130  			CTs: []tools.ContentTypeDesc{
   131  				{Type: fs.WorkfileType, ContentCnt: 10},
   132  				{Type: fs.ObjectType, ContentCnt: 500},
   133  			},
   134  			MountpathsCnt: 10,
   135  			ObjectSize:    cos.KiB,
   136  		}
   137  		out     = tools.PrepareObjects(t, desc)
   138  		counter = atomic.NewInt32(0)
   139  	)
   140  	defer os.RemoveAll(out.Dir)
   141  
   142  	opts := &mpather.JgroupOpts{
   143  		Bck: out.Bck,
   144  		CTs: []string{fs.ObjectType},
   145  		VisitObj: func(lom *core.LOM, buf []byte) error {
   146  			tassert.Errorf(t, lom.SizeBytes() == desc.ObjectSize, "incorrect object size (lom probably not loaded)")
   147  			tassert.Errorf(t, len(buf) == 0, "buffer expected to be empty")
   148  			counter.Inc()
   149  			return nil
   150  		},
   151  		DoLoad: mpather.Load,
   152  	}
   153  	jg := mpather.NewJoggerGroup(opts, cmn.GCO.Get(), "")
   154  
   155  	jg.Run()
   156  	<-jg.ListenFinished()
   157  
   158  	tassert.Errorf(
   159  		t, int(counter.Load()) == len(out.FQNs[fs.ObjectType]),
   160  		"invalid number of objects visited (%d vs %d)", counter.Load(), len(out.FQNs[fs.ObjectType]),
   161  	)
   162  
   163  	err := jg.Stop()
   164  	tassert.CheckFatal(t, err)
   165  }
   166  
   167  func TestJoggerGroupError(t *testing.T) {
   168  	var (
   169  		desc = tools.ObjectsDesc{
   170  			CTs: []tools.ContentTypeDesc{
   171  				{Type: fs.ObjectType, ContentCnt: 50},
   172  			},
   173  			MountpathsCnt: 4,
   174  			ObjectSize:    cos.KiB,
   175  		}
   176  		out     = tools.PrepareObjects(t, desc)
   177  		counter = atomic.NewInt32(0)
   178  	)
   179  	defer os.RemoveAll(out.Dir)
   180  
   181  	opts := &mpather.JgroupOpts{
   182  		Bck: out.Bck,
   183  		CTs: []string{fs.ObjectType},
   184  		VisitObj: func(_ *core.LOM, _ []byte) error {
   185  			counter.Inc()
   186  			return errors.New("oops")
   187  		},
   188  	}
   189  	jg := mpather.NewJoggerGroup(opts, cmn.GCO.Get(), "")
   190  	jg.Run()
   191  	<-jg.ListenFinished()
   192  
   193  	tassert.Errorf(
   194  		t, int(counter.Load()) <= desc.MountpathsCnt,
   195  		"joggers should not visit more than #mountpaths objects",
   196  	)
   197  
   198  	err := jg.Stop()
   199  	tassert.Errorf(t, err != nil && strings.Contains(err.Error(), "oops"), "expected an error")
   200  }
   201  
   202  // This test checks if single LOM error will cause all joggers to stop.
   203  func TestJoggerGroupOneErrorStopsAll(t *testing.T) {
   204  	var (
   205  		totalObjCnt = 5000
   206  		mpathsCnt   = 4
   207  		failAt      = int32(totalObjCnt/mpathsCnt) / 5 // Fail more or less at 20% of objects jogged.
   208  		desc        = tools.ObjectsDesc{
   209  			CTs: []tools.ContentTypeDesc{
   210  				{Type: fs.ObjectType, ContentCnt: totalObjCnt},
   211  			},
   212  			MountpathsCnt: mpathsCnt,
   213  			ObjectSize:    cos.KiB,
   214  		}
   215  		out = tools.PrepareObjects(t, desc)
   216  
   217  		mpaths      = fs.GetAvail()
   218  		counters    = make(map[string]*atomic.Int32, len(mpaths))
   219  		failOnMpath *fs.Mountpath
   220  		failed      atomic.Bool
   221  	)
   222  	defer os.RemoveAll(out.Dir)
   223  
   224  	for _, failOnMpath = range mpaths {
   225  		counters[failOnMpath.Path] = atomic.NewInt32(0)
   226  	}
   227  
   228  	opts := &mpather.JgroupOpts{
   229  		Bck: out.Bck,
   230  		CTs: []string{fs.ObjectType},
   231  		VisitObj: func(lom *core.LOM, _ []byte) error {
   232  			cnt := counters[lom.Mountpath().Path].Inc()
   233  
   234  			// Fail only once, on one mpath.
   235  			if cnt == failAt && failed.CAS(false, true) {
   236  				failOnMpath = lom.Mountpath()
   237  				return errors.New("oops")
   238  			}
   239  			return nil
   240  		},
   241  	}
   242  	jg := mpather.NewJoggerGroup(opts, cmn.GCO.Get(), "")
   243  	jg.Run()
   244  	<-jg.ListenFinished()
   245  
   246  	for mpath, counter := range counters {
   247  		// Expected at least one object to be skipped at each mountpath, when error occurred at 20% of objects jogged.
   248  		visitCount := counter.Load()
   249  		if mpath == failOnMpath.Path {
   250  			tassert.Fatalf(t, visitCount == failAt, "jogger on fail mpath %q expected to visit %d: visited %d",
   251  				mpath, failAt, visitCount)
   252  		}
   253  		tassert.Errorf(t, int(visitCount) <= out.MpathObjectsCnt[mpath],
   254  			"jogger on mpath %q expected to visit at most %d, visited %d",
   255  			mpath, out.MpathObjectsCnt[mpath], counter.Load())
   256  	}
   257  
   258  	err := jg.Stop()
   259  	tassert.Errorf(t, err != nil && strings.Contains(err.Error(), "oops"), "expected an error")
   260  }
   261  
   262  func TestJoggerGroupMultiContentTypes(t *testing.T) {
   263  	var (
   264  		cts  = []string{fs.ObjectType, fs.ECSliceType, fs.ECMetaType}
   265  		desc = tools.ObjectsDesc{
   266  			CTs: []tools.ContentTypeDesc{
   267  				{Type: fs.WorkfileType, ContentCnt: 10},
   268  				{Type: fs.ObjectType, ContentCnt: 541},
   269  				{Type: fs.ECSliceType, ContentCnt: 244},
   270  				{Type: fs.ECMetaType, ContentCnt: 405},
   271  			},
   272  			MountpathsCnt: 10,
   273  			ObjectSize:    cos.KiB,
   274  		}
   275  		out = tools.PrepareObjects(t, desc)
   276  	)
   277  	defer os.RemoveAll(out.Dir)
   278  
   279  	counters := make(map[string]*atomic.Int32, len(cts))
   280  	for _, ct := range cts {
   281  		counters[ct] = atomic.NewInt32(0)
   282  	}
   283  	opts := &mpather.JgroupOpts{
   284  		Bck: out.Bck,
   285  		CTs: cts,
   286  		VisitObj: func(_ *core.LOM, buf []byte) error {
   287  			tassert.Errorf(t, len(buf) == 0, "buffer expected to be empty")
   288  			counters[fs.ObjectType].Inc()
   289  			return nil
   290  		},
   291  		VisitCT: func(ct *core.CT, buf []byte) error {
   292  			tassert.Errorf(t, len(buf) == 0, "buffer expected to be empty")
   293  			counters[ct.ContentType()].Inc()
   294  			return nil
   295  		},
   296  	}
   297  	jg := mpather.NewJoggerGroup(opts, cmn.GCO.Get(), "")
   298  	jg.Run()
   299  	<-jg.ListenFinished()
   300  
   301  	// NOTE: No need to check `fs.WorkfileType == 0` since we would get panic when
   302  	//  increasing the counter (counter for `fs.WorkfileType` is not allocated).
   303  	for _, ct := range cts {
   304  		tassert.Errorf(
   305  			t, int(counters[ct].Load()) == len(out.FQNs[ct]),
   306  			"invalid number of %q visited (%d vs %d)", ct, counters[ct].Load(), len(out.FQNs[ct]),
   307  		)
   308  	}
   309  
   310  	err := jg.Stop()
   311  	tassert.CheckFatal(t, err)
   312  }