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 }