github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/erasure-encode_test.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"crypto/rand"
    24  	"io"
    25  	"testing"
    26  
    27  	"github.com/dustin/go-humanize"
    28  )
    29  
    30  type badDisk struct{ StorageAPI }
    31  
    32  func (a badDisk) String() string {
    33  	return "bad-disk"
    34  }
    35  
    36  func (a badDisk) AppendFile(ctx context.Context, volume string, path string, buf []byte) error {
    37  	return errFaultyDisk
    38  }
    39  
    40  func (a badDisk) ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error) {
    41  	return nil, errFaultyDisk
    42  }
    43  
    44  func (a badDisk) CreateFile(ctx context.Context, origvolume, volume, path string, size int64, reader io.Reader) error {
    45  	return errFaultyDisk
    46  }
    47  
    48  func (badDisk) Hostname() string {
    49  	return ""
    50  }
    51  
    52  const oneMiByte = 1 * humanize.MiByte
    53  
    54  var erasureEncodeTests = []struct {
    55  	dataBlocks                   int
    56  	onDisks, offDisks            int
    57  	blocksize, data              int64
    58  	offset                       int
    59  	algorithm                    BitrotAlgorithm
    60  	shouldFail, shouldFailQuorum bool
    61  }{
    62  	{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},                             // 0
    63  	{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 1, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false},                                 // 1
    64  	{dataBlocks: 4, onDisks: 8, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},                 // 2
    65  	{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},                    // 3
    66  	{dataBlocks: 6, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},                    // 4
    67  	{dataBlocks: 7, onDisks: 14, offDisks: 5, blocksize: int64(blockSizeV2), data: 0, offset: 0, shouldFail: false, algorithm: SHA256, shouldFailQuorum: false},                                        // 5
    68  	{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV2), data: 0, offset: 0, shouldFail: false, algorithm: DefaultBitrotAlgorithm, shouldFailQuorum: false},                        // 6
    69  	{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: true},                              // 7
    70  	{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: SHA256, shouldFail: false, shouldFailQuorum: true},                                  // 8
    71  	{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true},                 // 9
    72  	{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true},                 // 10
    73  	{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},                  // 11
    74  	{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},     // 12
    75  	{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
    76  	{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},               // 14
    77  	{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},                // 15
    78  	{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},                // 16
    79  	{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},              // 17
    80  	{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false},       // 18
    81  	{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true},                // 19
    82  }
    83  
    84  func TestErasureEncode(t *testing.T) {
    85  	for i, test := range erasureEncodeTests {
    86  		setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
    87  		if err != nil {
    88  			t.Fatalf("Test %d: failed to create test setup: %v", i, err)
    89  		}
    90  		disks := setup.disks
    91  		erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
    92  		if err != nil {
    93  			t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
    94  		}
    95  		buffer := make([]byte, test.blocksize, 2*test.blocksize)
    96  
    97  		data := make([]byte, test.data)
    98  		if _, err = io.ReadFull(rand.Reader, data); err != nil {
    99  			t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
   100  		}
   101  		writers := make([]io.Writer, len(disks))
   102  		for i, disk := range disks {
   103  			if disk == OfflineDisk {
   104  				continue
   105  			}
   106  			writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(int64(len(data[test.offset:]))), test.algorithm, erasure.ShardSize())
   107  		}
   108  		n, err := erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
   109  		closeBitrotWriters(writers)
   110  		if err != nil && !test.shouldFail {
   111  			t.Errorf("Test %d: should pass but failed with: %v", i, err)
   112  		}
   113  		if err == nil && test.shouldFail {
   114  			t.Errorf("Test %d: should fail but it passed", i)
   115  		}
   116  		for i, w := range writers {
   117  			if w == nil {
   118  				disks[i] = OfflineDisk
   119  			}
   120  		}
   121  		if err == nil {
   122  			if length := int64(len(data[test.offset:])); n != length {
   123  				t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
   124  			}
   125  			writers := make([]io.Writer, len(disks))
   126  			for i, disk := range disks {
   127  				if disk == nil {
   128  					continue
   129  				}
   130  				writers[i] = newBitrotWriter(disk, "", "testbucket", "object2", erasure.ShardFileSize(int64(len(data[test.offset:]))), test.algorithm, erasure.ShardSize())
   131  			}
   132  			for j := range disks[:test.offDisks] {
   133  				switch w := writers[j].(type) {
   134  				case *wholeBitrotWriter:
   135  					w.disk = badDisk{nil}
   136  				case *streamingBitrotWriter:
   137  					w.closeWithErr(errFaultyDisk)
   138  				}
   139  			}
   140  			if test.offDisks > 0 {
   141  				writers[0] = nil
   142  			}
   143  			n, err = erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
   144  			closeBitrotWriters(writers)
   145  			if err != nil && !test.shouldFailQuorum {
   146  				t.Errorf("Test %d: should pass but failed with: %v", i, err)
   147  			}
   148  			if err == nil && test.shouldFailQuorum {
   149  				t.Errorf("Test %d: should fail but it passed", i)
   150  			}
   151  			if err == nil {
   152  				if length := int64(len(data[test.offset:])); n != length {
   153  					t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
   154  				}
   155  			}
   156  		}
   157  	}
   158  }
   159  
   160  // Benchmarks
   161  
   162  func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
   163  	setup, err := newErasureTestSetup(b, data, parity, blockSizeV2)
   164  	if err != nil {
   165  		b.Fatalf("failed to create test setup: %v", err)
   166  	}
   167  	erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
   168  	if err != nil {
   169  		b.Fatalf("failed to create ErasureStorage: %v", err)
   170  	}
   171  	disks := setup.disks
   172  	buffer := make([]byte, blockSizeV2, 2*blockSizeV2)
   173  	content := make([]byte, size)
   174  
   175  	for i := 0; i < dataDown; i++ {
   176  		disks[i] = OfflineDisk
   177  	}
   178  	for i := data; i < data+parityDown; i++ {
   179  		disks[i] = OfflineDisk
   180  	}
   181  
   182  	b.ResetTimer()
   183  	b.SetBytes(size)
   184  	b.ReportAllocs()
   185  	for i := 0; i < b.N; i++ {
   186  		writers := make([]io.Writer, len(disks))
   187  		for i, disk := range disks {
   188  			if disk == OfflineDisk {
   189  				continue
   190  			}
   191  			disk.Delete(context.Background(), "testbucket", "object", DeleteOptions{
   192  				Recursive: false,
   193  				Immediate: false,
   194  			})
   195  			writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
   196  		}
   197  		_, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
   198  		closeBitrotWriters(writers)
   199  		if err != nil {
   200  			panic(err)
   201  		}
   202  	}
   203  }
   204  
   205  func BenchmarkErasureEncodeQuick(b *testing.B) {
   206  	const size = 12 * 1024 * 1024
   207  	b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, size, b) })
   208  	b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 1, size, b) })
   209  	b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 1, 0, size, b) })
   210  }
   211  
   212  func BenchmarkErasureEncode_4_64KB(b *testing.B) {
   213  	const size = 64 * 1024
   214  	b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, size, b) })
   215  	b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 1, size, b) })
   216  	b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 1, 0, size, b) })
   217  }
   218  
   219  func BenchmarkErasureEncode_8_20MB(b *testing.B) {
   220  	const size = 20 * 1024 * 1024
   221  	b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 0, size, b) })
   222  	b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 1, size, b) })
   223  	b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 1, 0, size, b) })
   224  	b.Run(" 0000|XXX0 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 3, size, b) })
   225  	b.Run(" XXX0|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 3, 0, size, b) })
   226  }
   227  
   228  func BenchmarkErasureEncode_12_30MB(b *testing.B) {
   229  	const size = 30 * 1024 * 1024
   230  	b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 0, size, b) })
   231  	b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 1, size, b) })
   232  	b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 1, 0, size, b) })
   233  	b.Run(" 000000|XXXXX0 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 5, size, b) })
   234  	b.Run(" XXXXX0|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 5, 0, size, b) })
   235  }
   236  
   237  func BenchmarkErasureEncode_16_40MB(b *testing.B) {
   238  	const size = 40 * 1024 * 1024
   239  	b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 0, size, b) })
   240  	b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 1, size, b) })
   241  	b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 1, 0, size, b) })
   242  	b.Run(" 00000000|XXXXXXX0 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 7, size, b) })
   243  	b.Run(" XXXXXXX0|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 7, 0, size, b) })
   244  }