storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/data-update-tracker_test.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2020 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 "bytes" 21 "context" 22 "fmt" 23 "io/ioutil" 24 "math/rand" 25 "os" 26 "path" 27 "path/filepath" 28 "sync" 29 "testing" 30 31 "storj.io/minio/cmd/logger" 32 "storj.io/minio/cmd/logger/message/log" 33 ) 34 35 type testLoggerI interface { 36 Helper() 37 Log(args ...interface{}) 38 } 39 40 type testingLogger struct { 41 mu sync.Mutex 42 t testLoggerI 43 } 44 45 func (t *testingLogger) Endpoint() string { 46 return "" 47 } 48 49 func (t *testingLogger) String() string { 50 return "" 51 } 52 53 func (t *testingLogger) Validate() error { 54 return nil 55 } 56 57 func (t *testingLogger) Send(entry interface{}, errKind string) error { 58 t.mu.Lock() 59 defer t.mu.Unlock() 60 if t.t == nil { 61 return nil 62 } 63 e, ok := entry.(log.Entry) 64 if !ok { 65 return fmt.Errorf("unexpected log entry structure %#v", entry) 66 } 67 68 t.t.Helper() 69 t.t.Log(e.Level, ":", errKind, e.Message) 70 return nil 71 } 72 73 func addTestingLogging(t testLoggerI) func() { 74 tl := &testingLogger{t: t} 75 logger.AddTarget(tl) 76 return func() { 77 tl.mu.Lock() 78 defer tl.mu.Unlock() 79 tl.t = nil 80 } 81 } 82 83 func TestDataUpdateTracker(t *testing.T) { 84 dut := newDataUpdateTracker() 85 // Change some defaults. 86 dut.debug = testing.Verbose() 87 dut.input = make(chan string) 88 dut.save = make(chan struct{}) 89 90 defer addTestingLogging(t)() 91 92 dut.Current.bf = dut.newBloomFilter() 93 94 tmpDir, err := ioutil.TempDir("", "TestDataUpdateTracker") 95 if err != nil { 96 t.Fatal(err) 97 } 98 err = os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, dataUpdateTrackerFilename)), os.ModePerm) 99 if err != nil { 100 t.Fatal(err) 101 } 102 defer os.RemoveAll(tmpDir) 103 ctx, cancel := context.WithCancel(context.Background()) 104 defer cancel() 105 dut.start(ctx, tmpDir) 106 107 var tests = []struct { 108 in string 109 check []string // if not empty, check against these instead. 110 exist bool 111 }{ 112 { 113 in: "bucket/directory/file.txt", 114 check: []string{"bucket", "bucket/", "/bucket", "bucket/directory", "bucket/directory/", "bucket/directory/file.txt", "/bucket/directory/file.txt"}, 115 exist: true, 116 }, 117 { 118 // System bucket 119 in: ".minio.sys/ignoreme/pls", 120 exist: false, 121 }, 122 { 123 // Not a valid bucket 124 in: "./bucket/okfile.txt", 125 check: []string{"./bucket/okfile.txt", "/bucket/okfile.txt", "bucket/okfile.txt"}, 126 exist: false, 127 }, 128 { 129 // Not a valid bucket 130 in: "æ/okfile.txt", 131 check: []string{"æ/okfile.txt", "æ/okfile.txt", "æ"}, 132 exist: false, 133 }, 134 { 135 in: "/bucket2/okfile2.txt", 136 check: []string{"./bucket2/okfile2.txt", "/bucket2/okfile2.txt", "bucket2/okfile2.txt", "bucket2"}, 137 exist: true, 138 }, 139 { 140 in: "/bucket3/prefix/okfile2.txt", 141 check: []string{"./bucket3/prefix/okfile2.txt", "/bucket3/prefix/okfile2.txt", "bucket3/prefix/okfile2.txt", "bucket3/prefix", "bucket3"}, 142 exist: true, 143 }, 144 } 145 for _, tt := range tests { 146 t.Run(tt.in, func(t *testing.T) { 147 dut.input <- tt.in 148 dut.input <- "" // Sending empty string ensures the previous is added to filter. 149 dut.mu.Lock() 150 defer dut.mu.Unlock() 151 if len(tt.check) == 0 { 152 got := dut.Current.bf.containsDir(tt.in) 153 if got != tt.exist { 154 // For unlimited tests this could lead to false positives, 155 // but it should be deterministic. 156 t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist) 157 } 158 return 159 } 160 for _, check := range tt.check { 161 got := dut.Current.bf.containsDir(check) 162 if got != tt.exist { 163 // For unlimited tests this could lead to false positives, 164 // but it should be deterministic. 165 t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist) 166 } 167 continue 168 } 169 }) 170 } 171 // Cycle to history 172 req := bloomFilterRequest{ 173 Oldest: 1, 174 Current: 2, 175 } 176 177 _, err = dut.cycleFilter(ctx, req) 178 if err != nil { 179 t.Fatal(err) 180 } 181 dut.input <- "cycle2/file.txt" 182 dut.input <- "" // Sending empty string ensures the previous is added to filter. 183 184 tests = append(tests, struct { 185 in string 186 check []string 187 exist bool 188 }{in: "cycle2/file.txt", exist: true}) 189 190 // Shut down 191 cancel() 192 <-dut.saveExited 193 194 if dut.current() != 2 { 195 t.Fatal("wrong current idx after save. want 2, got:", dut.current()) 196 } 197 198 ctx, cancel = context.WithCancel(context.Background()) 199 defer cancel() 200 201 // Reload... 202 dut = newDataUpdateTracker() 203 dut.start(ctx, tmpDir) 204 205 if dut.current() != 2 { 206 t.Fatal("current idx after load not preserved. want 2, got:", dut.current()) 207 } 208 req = bloomFilterRequest{ 209 Oldest: 1, 210 Current: 3, 211 } 212 bfr2, err := dut.cycleFilter(ctx, req) 213 if err != nil { 214 t.Fatal(err) 215 } 216 if !bfr2.Complete { 217 t.Fatal("Wanted complete, didn't get it") 218 } 219 if bfr2.CurrentIdx != 3 { 220 t.Fatal("wanted index 3, got", bfr2.CurrentIdx) 221 } 222 if bfr2.OldestIdx != 1 { 223 t.Fatal("wanted oldest index 3, got", bfr2.OldestIdx) 224 } 225 226 // Rerun test with returned bfr2 227 bf := dut.newBloomFilter() 228 _, err = bf.ReadFrom(bytes.NewReader(bfr2.Filter)) 229 if err != nil { 230 t.Fatal(err) 231 } 232 for _, tt := range tests { 233 t.Run(tt.in+"-reloaded", func(t *testing.T) { 234 if len(tt.check) == 0 { 235 got := bf.containsDir(tt.in) 236 if got != tt.exist { 237 // For unlimited tests this could lead to false positives, 238 // but it should be deterministic. 239 t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist) 240 } 241 return 242 } 243 for _, check := range tt.check { 244 got := bf.containsDir(check) 245 if got != tt.exist { 246 // For unlimited tests this could lead to false positives, 247 // but it should be deterministic. 248 t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist) 249 } 250 continue 251 } 252 }) 253 } 254 } 255 256 func BenchmarkDataUpdateTracker(b *testing.B) { 257 dut := newDataUpdateTracker() 258 // Change some defaults. 259 dut.debug = false 260 dut.input = make(chan string) 261 dut.save = make(chan struct{}) 262 263 defer addTestingLogging(b)() 264 265 dut.Current.bf = dut.newBloomFilter() 266 // We do this unbuffered. This will very significantly reduce throughput, so this is a worst case. 267 ctx, cancel := context.WithCancel(context.Background()) 268 defer cancel() 269 go dut.startCollector(ctx) 270 input := make([]string, 1000) 271 rng := rand.New(rand.NewSource(0xabad1dea)) 272 tmp := []string{"bucket", "aprefix", "nextprefixlevel", "maybeobjname", "evendeeper", "ok-one-morelevel", "final.object"} 273 for i := range input { 274 tmp := tmp[:1+rng.Intn(cap(tmp)-1)] 275 input[i] = path.Join(tmp...) 276 } 277 b.SetBytes(1) 278 b.ResetTimer() 279 b.ReportAllocs() 280 for i := 0; i < b.N; i++ { 281 dut.input <- input[rng.Intn(len(input))] 282 } 283 }