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  }