github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/multithread_test.go (about)

     1  package operations
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/rclone/rclone/fs/accounting"
    13  	"github.com/rclone/rclone/fs/hash"
    14  	"github.com/rclone/rclone/fs/object"
    15  	"github.com/rclone/rclone/fstest/mockfs"
    16  	"github.com/rclone/rclone/fstest/mockobject"
    17  	"github.com/rclone/rclone/lib/random"
    18  
    19  	"github.com/rclone/rclone/fs"
    20  	"github.com/rclone/rclone/fstest"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  func TestDoMultiThreadCopy(t *testing.T) {
    26  	ctx := context.Background()
    27  	ci := fs.GetConfig(ctx)
    28  	f, err := mockfs.NewFs(ctx, "potato", "", nil)
    29  	require.NoError(t, err)
    30  	src := mockobject.New("file.txt").WithContent([]byte(random.String(100)), mockobject.SeekModeNone)
    31  	srcFs, err := mockfs.NewFs(ctx, "sausage", "", nil)
    32  	require.NoError(t, err)
    33  	src.SetFs(srcFs)
    34  
    35  	oldStreams := ci.MultiThreadStreams
    36  	oldCutoff := ci.MultiThreadCutoff
    37  	oldIsSet := ci.MultiThreadSet
    38  	defer func() {
    39  		ci.MultiThreadStreams = oldStreams
    40  		ci.MultiThreadCutoff = oldCutoff
    41  		ci.MultiThreadSet = oldIsSet
    42  	}()
    43  
    44  	ci.MultiThreadStreams, ci.MultiThreadCutoff = 4, 50
    45  	ci.MultiThreadSet = false
    46  
    47  	nullWriterAt := func(ctx context.Context, remote string, size int64) (fs.WriterAtCloser, error) {
    48  		panic("don't call me")
    49  	}
    50  	f.Features().OpenWriterAt = nullWriterAt
    51  
    52  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    53  
    54  	ci.MultiThreadStreams = 0
    55  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    56  	ci.MultiThreadStreams = 1
    57  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    58  	ci.MultiThreadStreams = 2
    59  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    60  
    61  	ci.MultiThreadCutoff = 200
    62  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    63  	ci.MultiThreadCutoff = 101
    64  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    65  	ci.MultiThreadCutoff = 100
    66  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    67  
    68  	f.Features().OpenWriterAt = nil
    69  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    70  	f.Features().OpenWriterAt = nullWriterAt
    71  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    72  
    73  	f.Features().IsLocal = true
    74  	srcFs.Features().IsLocal = true
    75  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    76  	ci.MultiThreadSet = true
    77  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    78  	ci.MultiThreadSet = false
    79  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    80  	srcFs.Features().IsLocal = false
    81  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    82  	srcFs.Features().IsLocal = true
    83  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    84  	f.Features().IsLocal = false
    85  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    86  	srcFs.Features().IsLocal = false
    87  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    88  
    89  	srcFs.Features().NoMultiThreading = true
    90  	assert.False(t, doMultiThreadCopy(ctx, f, src))
    91  	srcFs.Features().NoMultiThreading = false
    92  	assert.True(t, doMultiThreadCopy(ctx, f, src))
    93  }
    94  
    95  func TestMultithreadCalculateNumChunks(t *testing.T) {
    96  	for _, test := range []struct {
    97  		size          int64
    98  		chunkSize     int64
    99  		wantNumChunks int
   100  	}{
   101  		{size: 1, chunkSize: multithreadChunkSize, wantNumChunks: 1},
   102  		{size: 1 << 20, chunkSize: 1, wantNumChunks: 1 << 20},
   103  		{size: 1 << 20, chunkSize: 2, wantNumChunks: 1 << 19},
   104  		{size: (1 << 20) + 1, chunkSize: 2, wantNumChunks: (1 << 19) + 1},
   105  		{size: (1 << 20) - 1, chunkSize: 2, wantNumChunks: 1 << 19},
   106  	} {
   107  		t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
   108  			mc := &multiThreadCopyState{
   109  				size: test.size,
   110  			}
   111  			mc.numChunks = calculateNumChunks(test.size, test.chunkSize)
   112  			assert.Equal(t, test.wantNumChunks, mc.numChunks)
   113  		})
   114  	}
   115  }
   116  
   117  // Skip if not multithread, returning the chunkSize otherwise
   118  func skipIfNotMultithread(ctx context.Context, t *testing.T, r *fstest.Run) int {
   119  	features := r.Fremote.Features()
   120  	if features.OpenChunkWriter == nil && features.OpenWriterAt == nil {
   121  		t.Skip("multithread writing not supported")
   122  	}
   123  
   124  	// Only support one hash otherwise we end up spending a huge amount of CPU on hashing!
   125  	oldHashes := hash.SupportOnly([]hash.Type{r.Fremote.Hashes().GetOne()})
   126  	t.Cleanup(func() {
   127  		_ = hash.SupportOnly(oldHashes)
   128  	})
   129  
   130  	ci := fs.GetConfig(ctx)
   131  	chunkSize := int(ci.MultiThreadChunkSize)
   132  	if features.OpenChunkWriter != nil {
   133  		//OpenChunkWriter func(ctx context.Context, remote string, src ObjectInfo, options ...OpenOption) (info ChunkWriterInfo, writer ChunkWriter, err error)
   134  		const fileName = "chunksize-probe"
   135  		src := object.NewStaticObjectInfo(fileName, time.Now(), int64(100*fs.Mebi), true, nil, nil)
   136  		info, writer, err := features.OpenChunkWriter(ctx, fileName, src)
   137  		require.NoError(t, err)
   138  		chunkSize = int(info.ChunkSize)
   139  		err = writer.Abort(ctx)
   140  		require.NoError(t, err)
   141  	}
   142  	return chunkSize
   143  }
   144  
   145  func TestMultithreadCopy(t *testing.T) {
   146  	r := fstest.NewRun(t)
   147  	ctx := context.Background()
   148  	chunkSize := skipIfNotMultithread(ctx, t, r)
   149  	// Check every other transfer for metadata
   150  	checkMetadata := false
   151  	ctx, ci := fs.AddConfig(ctx)
   152  
   153  	for _, upload := range []bool{false, true} {
   154  		for _, test := range []struct {
   155  			size    int
   156  			streams int
   157  		}{
   158  			{size: chunkSize*2 - 1, streams: 2},
   159  			{size: chunkSize * 2, streams: 2},
   160  			{size: chunkSize*2 + 1, streams: 2},
   161  		} {
   162  			checkMetadata = !checkMetadata
   163  			ci.Metadata = checkMetadata
   164  			fileName := fmt.Sprintf("test-multithread-copy-%v-%d-%d", upload, test.size, test.streams)
   165  			t.Run(fmt.Sprintf("upload=%v,size=%v,streams=%v", upload, test.size, test.streams), func(t *testing.T) {
   166  				if *fstest.SizeLimit > 0 && int64(test.size) > *fstest.SizeLimit {
   167  					t.Skipf("exceeded file size limit %d > %d", test.size, *fstest.SizeLimit)
   168  				}
   169  				var (
   170  					contents     = random.String(test.size)
   171  					t1           = fstest.Time("2001-02-03T04:05:06.499999999Z")
   172  					file1        fstest.Item
   173  					src, dst     fs.Object
   174  					err          error
   175  					testMetadata = fs.Metadata{
   176  						// System metadata supported by all backends
   177  						"mtime": t1.Format(time.RFC3339Nano),
   178  						// User metadata
   179  						"potato": "jersey",
   180  					}
   181  				)
   182  
   183  				var fSrc, fDst fs.Fs
   184  				if upload {
   185  					file1 = r.WriteFile(fileName, contents, t1)
   186  					r.CheckRemoteItems(t)
   187  					r.CheckLocalItems(t, file1)
   188  					fDst, fSrc = r.Fremote, r.Flocal
   189  				} else {
   190  					file1 = r.WriteObject(ctx, fileName, contents, t1)
   191  					r.CheckRemoteItems(t, file1)
   192  					r.CheckLocalItems(t)
   193  					fDst, fSrc = r.Flocal, r.Fremote
   194  				}
   195  				src, err = fSrc.NewObject(ctx, fileName)
   196  				require.NoError(t, err)
   197  
   198  				do, canSetMetadata := src.(fs.SetMetadataer)
   199  				if checkMetadata && canSetMetadata {
   200  					// Set metadata on the source if required
   201  					err := do.SetMetadata(ctx, testMetadata)
   202  					if err == fs.ErrorNotImplemented {
   203  						canSetMetadata = false
   204  					} else {
   205  						require.NoError(t, err)
   206  						fstest.CheckEntryMetadata(ctx, t, r.Flocal, src, testMetadata)
   207  					}
   208  				}
   209  
   210  				accounting.GlobalStats().ResetCounters()
   211  				tr := accounting.GlobalStats().NewTransfer(src, nil)
   212  
   213  				defer func() {
   214  					tr.Done(ctx, err)
   215  				}()
   216  
   217  				dst, err = multiThreadCopy(ctx, fDst, fileName, src, test.streams, tr)
   218  				require.NoError(t, err)
   219  
   220  				assert.Equal(t, src.Size(), dst.Size())
   221  				assert.Equal(t, fileName, dst.Remote())
   222  				fstest.CheckListingWithPrecision(t, fSrc, []fstest.Item{file1}, nil, fs.GetModifyWindow(ctx, fDst, fSrc))
   223  				fstest.CheckListingWithPrecision(t, fDst, []fstest.Item{file1}, nil, fs.GetModifyWindow(ctx, fDst, fSrc))
   224  
   225  				if checkMetadata && canSetMetadata && fDst.Features().ReadMetadata {
   226  					fstest.CheckEntryMetadata(ctx, t, fDst, dst, testMetadata)
   227  				}
   228  
   229  				require.NoError(t, dst.Remove(ctx))
   230  				require.NoError(t, src.Remove(ctx))
   231  
   232  			})
   233  		}
   234  	}
   235  }
   236  
   237  type errorObject struct {
   238  	fs.Object
   239  	size int64
   240  	wg   *sync.WaitGroup
   241  }
   242  
   243  // Open opens the file for read.  Call Close() on the returned io.ReadCloser
   244  //
   245  // Remember this is called multiple times whenever the backend seeks (eg having read checksum)
   246  func (o errorObject) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
   247  	fs.Debugf(nil, "Open with options = %v", options)
   248  	rc, err := o.Object.Open(ctx, options...)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	// Return an error reader for the second segment
   253  	for _, option := range options {
   254  		if ropt, ok := option.(*fs.RangeOption); ok {
   255  			end := ropt.End + 1
   256  			if end >= o.size {
   257  				// Give the other chunks a chance to start
   258  				time.Sleep(time.Second)
   259  				// Wait for chunks to upload first
   260  				o.wg.Wait()
   261  				fs.Debugf(nil, "Returning error reader")
   262  				return errorReadCloser{rc}, nil
   263  			}
   264  		}
   265  	}
   266  	o.wg.Add(1)
   267  	return wgReadCloser{rc, o.wg}, nil
   268  }
   269  
   270  type errorReadCloser struct {
   271  	io.ReadCloser
   272  }
   273  
   274  func (rc errorReadCloser) Read(p []byte) (n int, err error) {
   275  	fs.Debugf(nil, "BOOM: simulated read failure")
   276  	return 0, errors.New("BOOM: simulated read failure")
   277  }
   278  
   279  type wgReadCloser struct {
   280  	io.ReadCloser
   281  	wg *sync.WaitGroup
   282  }
   283  
   284  func (rc wgReadCloser) Close() (err error) {
   285  	rc.wg.Done()
   286  	return rc.ReadCloser.Close()
   287  }
   288  
   289  // Make sure aborting the multi-thread copy doesn't overwrite an existing file.
   290  func TestMultithreadCopyAbort(t *testing.T) {
   291  	r := fstest.NewRun(t)
   292  	ctx := context.Background()
   293  	chunkSize := skipIfNotMultithread(ctx, t, r)
   294  	size := 2*chunkSize + 1
   295  
   296  	if *fstest.SizeLimit > 0 && int64(size) > *fstest.SizeLimit {
   297  		t.Skipf("exceeded file size limit %d > %d", size, *fstest.SizeLimit)
   298  	}
   299  
   300  	// first write a canary file which we are trying not to overwrite
   301  	const fileName = "test-multithread-abort"
   302  	contents := random.String(100)
   303  	t1 := fstest.Time("2001-02-03T04:05:06.499999999Z")
   304  	canary := r.WriteObject(ctx, fileName, contents, t1)
   305  	r.CheckRemoteItems(t, canary)
   306  
   307  	// Now write a local file to upload
   308  	file1 := r.WriteFile(fileName, random.String(size), t1)
   309  	r.CheckLocalItems(t, file1)
   310  
   311  	src, err := r.Flocal.NewObject(ctx, fileName)
   312  	require.NoError(t, err)
   313  	accounting.GlobalStats().ResetCounters()
   314  	tr := accounting.GlobalStats().NewTransfer(src, nil)
   315  
   316  	defer func() {
   317  		tr.Done(ctx, err)
   318  	}()
   319  	wg := new(sync.WaitGroup)
   320  	dst, err := multiThreadCopy(ctx, r.Fremote, fileName, errorObject{src, int64(size), wg}, 1, tr)
   321  	assert.Error(t, err)
   322  	assert.Nil(t, dst)
   323  
   324  	if r.Fremote.Features().PartialUploads {
   325  		r.CheckRemoteItems(t)
   326  
   327  	} else {
   328  		r.CheckRemoteItems(t, canary)
   329  		o, err := r.Fremote.NewObject(ctx, fileName)
   330  		require.NoError(t, err)
   331  		require.NoError(t, o.Remove(ctx))
   332  	}
   333  }