github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/fs/operations/multithread.go (about) 1 package operations 2 3 import ( 4 "context" 5 "io" 6 7 "github.com/ncw/rclone/fs" 8 "github.com/ncw/rclone/fs/accounting" 9 "github.com/pkg/errors" 10 "golang.org/x/sync/errgroup" 11 ) 12 13 const ( 14 multithreadChunkSize = 64 << 10 15 multithreadChunkSizeMask = multithreadChunkSize - 1 16 multithreadBufferSize = 32 * 1024 17 ) 18 19 // state for a multi-thread copy 20 type multiThreadCopyState struct { 21 ctx context.Context 22 partSize int64 23 size int64 24 wc fs.WriterAtCloser 25 src fs.Object 26 acc *accounting.Account 27 streams int 28 } 29 30 // Copy a single stream into place 31 func (mc *multiThreadCopyState) copyStream(ctx context.Context, stream int) (err error) { 32 defer func() { 33 if err != nil { 34 fs.Debugf(mc.src, "multi-thread copy: stream %d/%d failed: %v", stream+1, mc.streams, err) 35 } 36 }() 37 start := int64(stream) * mc.partSize 38 if start >= mc.size { 39 return nil 40 } 41 end := start + mc.partSize 42 if end > mc.size { 43 end = mc.size 44 } 45 46 fs.Debugf(mc.src, "multi-thread copy: stream %d/%d (%d-%d) size %v starting", stream+1, mc.streams, start, end, fs.SizeSuffix(end-start)) 47 48 rc, err := newReOpen(ctx, mc.src, nil, &fs.RangeOption{Start: start, End: end - 1}, fs.Config.LowLevelRetries) 49 if err != nil { 50 return errors.Wrap(err, "multpart copy: failed to open source") 51 } 52 defer fs.CheckClose(rc, &err) 53 54 // Copy the data 55 buf := make([]byte, multithreadBufferSize) 56 offset := start 57 for { 58 // Check if context cancelled and exit if so 59 if mc.ctx.Err() != nil { 60 return mc.ctx.Err() 61 } 62 nr, er := rc.Read(buf) 63 if nr > 0 { 64 err = mc.acc.AccountRead(nr) 65 if err != nil { 66 return errors.Wrap(err, "multpart copy: accounting failed") 67 } 68 nw, ew := mc.wc.WriteAt(buf[0:nr], offset) 69 if nw > 0 { 70 offset += int64(nw) 71 } 72 if ew != nil { 73 return errors.Wrap(ew, "multpart copy: write failed") 74 } 75 if nr != nw { 76 return errors.Wrap(io.ErrShortWrite, "multpart copy") 77 } 78 } 79 if er != nil { 80 if er != io.EOF { 81 return errors.Wrap(er, "multpart copy: read failed") 82 } 83 break 84 } 85 } 86 87 if offset != end { 88 return errors.Errorf("multpart copy: wrote %d bytes but expected to write %d", offset-start, end-start) 89 } 90 91 fs.Debugf(mc.src, "multi-thread copy: stream %d/%d (%d-%d) size %v finished", stream+1, mc.streams, start, end, fs.SizeSuffix(end-start)) 92 return nil 93 } 94 95 // Calculate the chunk sizes and updated number of streams 96 func (mc *multiThreadCopyState) calculateChunks() { 97 partSize := mc.size / int64(mc.streams) 98 // Round partition size up so partSize * streams >= size 99 if (mc.size % int64(mc.streams)) != 0 { 100 partSize++ 101 } 102 // round partSize up to nearest multithreadChunkSize boundary 103 mc.partSize = (partSize + multithreadChunkSizeMask) &^ multithreadChunkSizeMask 104 // recalculate number of streams 105 mc.streams = int(mc.size / mc.partSize) 106 // round streams up so partSize * streams >= size 107 if (mc.size % mc.partSize) != 0 { 108 mc.streams++ 109 } 110 } 111 112 // Copy src to (f, remote) using streams download threads and the OpenWriterAt feature 113 func multiThreadCopy(ctx context.Context, f fs.Fs, remote string, src fs.Object, streams int) (newDst fs.Object, err error) { 114 openWriterAt := f.Features().OpenWriterAt 115 if openWriterAt == nil { 116 return nil, errors.New("multi-thread copy: OpenWriterAt not supported") 117 } 118 if src.Size() < 0 { 119 return nil, errors.New("multi-thread copy: can't copy unknown sized file") 120 } 121 if src.Size() == 0 { 122 return nil, errors.New("multi-thread copy: can't copy zero sized file") 123 } 124 125 g, gCtx := errgroup.WithContext(ctx) 126 mc := &multiThreadCopyState{ 127 ctx: gCtx, 128 size: src.Size(), 129 src: src, 130 streams: streams, 131 } 132 mc.calculateChunks() 133 134 // Make accounting 135 mc.acc = accounting.NewAccount(nil, src) 136 defer fs.CheckClose(mc.acc, &err) 137 138 // create write file handle 139 mc.wc, err = openWriterAt(gCtx, remote, mc.size) 140 if err != nil { 141 return nil, errors.Wrap(err, "multpart copy: failed to open destination") 142 } 143 defer fs.CheckClose(mc.wc, &err) 144 145 fs.Debugf(src, "Starting multi-thread copy with %d parts of size %v", mc.streams, fs.SizeSuffix(mc.partSize)) 146 for stream := 0; stream < mc.streams; stream++ { 147 stream := stream 148 g.Go(func() (err error) { 149 return mc.copyStream(gCtx, stream) 150 }) 151 } 152 err = g.Wait() 153 if err != nil { 154 return nil, err 155 } 156 157 obj, err := f.NewObject(ctx, remote) 158 if err != nil { 159 return nil, errors.Wrap(err, "multi-thread copy: failed to find object after copy") 160 } 161 162 err = obj.SetModTime(ctx, src.ModTime(ctx)) 163 switch err { 164 case nil, fs.ErrorCantSetModTime, fs.ErrorCantSetModTimeWithoutDelete: 165 default: 166 return nil, errors.Wrap(err, "multi-thread copy: failed to set modification time") 167 } 168 169 fs.Debugf(src, "Finished multi-thread copy with %d parts of size %v", mc.streams, fs.SizeSuffix(mc.partSize)) 170 return obj, nil 171 }