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 }