gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/downloaddestination.go (about)

     1  package renter
     2  
     3  // Downloads can be written directly to a file, can be written to an http
     4  // stream, or can be written to an in-memory buffer. The core download loop only
     5  // has the concept of writing using WriteAt, and then calling Close when the
     6  // download is complete.
     7  //
     8  // To support streaming and writing to memory buffers, the downloadDestination
     9  // interface exists. It is used to map things like a []byte or an io.WriteCloser
    10  // to a downloadDestination. This interface is implemented by:
    11  //		+ os.File
    12  //		+ downloadDestinationBuffer (an alias of a []byte)
    13  //		+ downloadDestinationWriteCloser (created using an io.WriteCloser)
    14  //
    15  // There is also a helper function to convert an io.Writer to an io.WriteCloser,
    16  // so that an io.Writer can be used to create a downloadDestinationWriteCloser
    17  // as well.
    18  
    19  import (
    20  	"bufio"
    21  	"io"
    22  	"os"
    23  	"sync"
    24  	"time"
    25  
    26  	"gitlab.com/NebulousLabs/errors"
    27  	"gitlab.com/NebulousLabs/fastrand"
    28  	"gitlab.com/SkynetLabs/skyd/skymodules"
    29  	"go.sia.tech/siad/modules"
    30  )
    31  
    32  // skipWriter is a helper type that ignores the first 'skip' bytes written to it.
    33  type skipWriter struct {
    34  	writer io.Writer
    35  	skip   int
    36  }
    37  
    38  // Write will write bytes to the skipWriter, being sure to skip over any bytes
    39  // which the skipWriter was initialized to skip
    40  func (sw *skipWriter) Write(p []byte) (int, error) {
    41  	if sw.skip == 0 {
    42  		return sw.writer.Write(p)
    43  	} else if sw.skip > len(p) {
    44  		sw.skip -= len(p)
    45  		return len(p), nil
    46  	}
    47  	n, err := sw.writer.Write(p[sw.skip:])
    48  	n += sw.skip
    49  	sw.skip = 0
    50  	return n, err
    51  }
    52  
    53  // sectionWriter implements Write on a section
    54  // of an underlying WriterAt.
    55  type sectionWriter struct {
    56  	w     io.WriterAt
    57  	base  int64
    58  	off   int64
    59  	limit int64
    60  }
    61  
    62  // errSectionWriteOutOfBounds is an error returned by the section writer if a
    63  // write would cross the boundaries between section.
    64  var errSectionWriteOutOfBounds = errors.New("section write is out of bounds")
    65  
    66  // NewSectionWriter returns a sectionWriter that writes to w
    67  // starting at offset off and stops with EOF after n bytes.
    68  func NewSectionWriter(w io.WriterAt, off int64, n int64) *sectionWriter {
    69  	return &sectionWriter{w, off, off, off + n}
    70  }
    71  
    72  // Write implements the io.Writer interface using WriteAt.
    73  func (s *sectionWriter) Write(p []byte) (n int, err error) {
    74  	if s.off >= s.limit || int64(len(p)) > s.limit-s.off {
    75  		return 0, errSectionWriteOutOfBounds
    76  	}
    77  	n, err = s.w.WriteAt(p, s.off)
    78  	s.off += int64(n)
    79  	return
    80  }
    81  
    82  // downloadDestination is the interface that receives the data recovered by the
    83  // download process. The call to WritePieces is in `threadedRecoverLogicalData`.
    84  //
    85  // The downloadDestination interface takes a bunch of pieces because different
    86  // types of destinations will prefer receiving pieces over receiving continuous
    87  // data. The destinations that prefer taking continuous data can have the
    88  // WritePieces method of the interface convert the pieces into continuous data.
    89  type downloadDestination interface {
    90  	// WritePieces takes the set of pieces from the chunk as input. There should
    91  	// be at least `minPieces` pieces, but they do not need to be the data
    92  	// pieces - the downloadDestination can check and determine if a recovery is
    93  	// required.
    94  	//
    95  	// The pieces are provided decrypted.
    96  	WritePieces(ec skymodules.ErasureCoder, pieces [][]byte, dataOffset uint64, writeOffset int64, length uint64) error
    97  }
    98  
    99  // downloadDestinationBuffer writes logical chunk data to an in-memory buffer.
   100  // This buffer is primarily used when performing repairs on uploads.
   101  type downloadDestinationBuffer struct {
   102  	pieces [][]byte
   103  }
   104  
   105  // NewDownloadDestinationBuffer allocates the necessary number of shards for
   106  // the downloadDestinationBuffer and returns the new buffer.
   107  func NewDownloadDestinationBuffer() *downloadDestinationBuffer {
   108  	return &downloadDestinationBuffer{}
   109  }
   110  
   111  // WritePieces stores the provided pieces for later processing.
   112  func (dw *downloadDestinationBuffer) WritePieces(_ skymodules.ErasureCoder, pieces [][]byte, _ uint64, _ int64, _ uint64) error {
   113  	dw.pieces = pieces
   114  	return nil
   115  }
   116  
   117  // downloadDestinationFile wraps an os.File into a downloadDestination.
   118  type downloadDestinationFile struct {
   119  	deps            modules.Dependencies
   120  	f               *os.File
   121  	staticChunkSize int64
   122  }
   123  
   124  // Close implements the io.Closer interface for downloadDestinationFile.
   125  func (ddf *downloadDestinationFile) Close() error {
   126  	return ddf.f.Close()
   127  }
   128  
   129  // WritePieces will decode the pieces and write them to a file at the provided
   130  // offset, using the provided length.
   131  func (ddf *downloadDestinationFile) WritePieces(ec skymodules.ErasureCoder, pieces [][]byte, dataOffset uint64, offset int64, length uint64) error {
   132  	sw := NewSectionWriter(ddf.f, offset, ddf.staticChunkSize)
   133  	if ddf.deps.Disrupt("PostponeWritePiecesRecovery") {
   134  		time.Sleep(time.Duration(fastrand.Intn(1000)) * time.Millisecond)
   135  	}
   136  	skipWriter := &skipWriter{
   137  		writer: sw,
   138  		skip:   int(dataOffset),
   139  	}
   140  	bufioWriter := bufio.NewWriter(skipWriter)
   141  	err := ec.Recover(pieces, dataOffset+length, bufioWriter)
   142  	err2 := bufioWriter.Flush()
   143  	return errors.AddContext(errors.Compose(err, err2), "unable to write pieces to destination file")
   144  }
   145  
   146  // downloadDestinationWriter is a downloadDestination that writes to an
   147  // underlying data stream. The data stream is expecting sequential data while
   148  // the download chunks will be written in an arbitrary order using calls to
   149  // WriteAt. We need to block the calls to WriteAt until all prior data has been
   150  // written.
   151  //
   152  // NOTE: If the caller accidentally leaves a gap between calls to WriteAt, for
   153  // example writes bytes 0-100 and then writes bytes 110-200, and accidentally
   154  // never writes bytes 100-110, the downloadDestinationWriteCloser will block
   155  // forever waiting for those gap bytes to be written.
   156  //
   157  // NOTE: Calling WriteAt has linear time performance in the number of concurrent
   158  // calls to WriteAt.
   159  type downloadDestinationWriter struct {
   160  	closed    bool
   161  	mu        sync.Mutex // Protects the underlying data structures.
   162  	progress  int64      // How much data has been written yet.
   163  	io.Writer            // The underlying writer.
   164  
   165  	// A list of write calls and their corresponding locks. When one write call
   166  	// completes, it'll search through the list of write calls for the next one.
   167  	// The next write call can be unblocked by unlocking the corresponding mutex
   168  	// in the next array.
   169  	blockingWriteCalls   []int64 // A list of write calls that are waiting for their turn
   170  	blockingWriteSignals []*sync.Mutex
   171  }
   172  
   173  var (
   174  	// errClosedStream gets returned if the stream was closed but we are trying
   175  	// to write.
   176  	errClosedStream = errors.New("unable to write because stream has been closed")
   177  
   178  	// errOffsetAlreadyWritten gets returned if a call to WriteAt tries to write
   179  	// to a place in the stream which has already had data written to it.
   180  	errOffsetAlreadyWritten = errors.New("cannot write to that offset in stream, data already written")
   181  )
   182  
   183  // newDownloadDestinationWriter takes an io.Writer and converts it
   184  // into a downloadDestination.
   185  func newDownloadDestinationWriter(w io.Writer) *downloadDestinationWriter {
   186  	return &downloadDestinationWriter{Writer: w}
   187  }
   188  
   189  // unblockNextWrites will iterate over all of the blocking write calls and
   190  // unblock any whose offsets have been reached by the current progress of the
   191  // stream.
   192  //
   193  // NOTE: unblockNextWrites has linear time performance in the number of currently
   194  // blocking calls.
   195  func (ddw *downloadDestinationWriter) unblockNextWrites() {
   196  	for i, offset := range ddw.blockingWriteCalls {
   197  		if offset <= ddw.progress {
   198  			ddw.blockingWriteSignals[i].Unlock()
   199  			ddw.blockingWriteCalls = append(ddw.blockingWriteCalls[0:i], ddw.blockingWriteCalls[i+1:]...)
   200  			ddw.blockingWriteSignals = append(ddw.blockingWriteSignals[0:i], ddw.blockingWriteSignals[i+1:]...)
   201  		}
   202  	}
   203  }
   204  
   205  // Close will unblock any hanging calls to WriteAt, and then call Close on the
   206  // underlying WriteCloser.
   207  func (ddw *downloadDestinationWriter) Close() error {
   208  	ddw.mu.Lock()
   209  	if ddw.closed {
   210  		ddw.mu.Unlock()
   211  		return errClosedStream
   212  	}
   213  	ddw.closed = true
   214  	for i := range ddw.blockingWriteSignals {
   215  		ddw.blockingWriteSignals[i].Unlock()
   216  	}
   217  	ddw.mu.Unlock()
   218  	return nil
   219  }
   220  
   221  // WritePieces will block until the stream has progressed to 'offset', and then
   222  // decode the pieces and write them. An error will be returned if the stream has
   223  // already progressed beyond 'offset'.
   224  func (ddw *downloadDestinationWriter) WritePieces(ec skymodules.ErasureCoder, pieces [][]byte, dataOffset uint64, offset int64, length uint64) error {
   225  	write := func() error {
   226  		// Error if the stream has been closed.
   227  		if ddw.closed {
   228  			return errClosedStream
   229  		}
   230  		// Error if the stream has progressed beyond 'offset'.
   231  		if offset < ddw.progress {
   232  			return errOffsetAlreadyWritten
   233  		}
   234  
   235  		// Write the data to the stream, and the update the progress and unblock
   236  		// the next write.
   237  		err := ec.Recover(pieces, dataOffset+length, &skipWriter{writer: ddw, skip: int(dataOffset)})
   238  		ddw.progress += int64(length)
   239  		ddw.unblockNextWrites()
   240  		return err
   241  	}
   242  
   243  	ddw.mu.Lock()
   244  	// Attempt to write if the stream progress is at or beyond the offset. The
   245  	// write call will perform error handling.
   246  	if offset <= ddw.progress {
   247  		err := write()
   248  		ddw.mu.Unlock()
   249  		return err
   250  	}
   251  
   252  	// The stream has not yet progressed to 'offset'. We will block until the
   253  	// stream has made progress. We perform the block by creating a
   254  	// thread-specific mutex 'myMu' and adding it to the object's list of
   255  	// blocking threads. When other threads successfully call WriteAt, they will
   256  	// reference this list and unblock any which have enough progress. The
   257  	// result is a somewhat strange construction where we lock myMu twice in a
   258  	// row, but between those two calls to lock, we put myMu in a place where
   259  	// another thread can unlock myMu.
   260  	//
   261  	// myMu will be unblocked when another thread calls 'unblockNextWrites'.
   262  	myMu := new(sync.Mutex)
   263  	myMu.Lock()
   264  	ddw.blockingWriteCalls = append(ddw.blockingWriteCalls, offset)
   265  	ddw.blockingWriteSignals = append(ddw.blockingWriteSignals, myMu)
   266  	ddw.mu.Unlock()
   267  	myMu.Lock()
   268  	ddw.mu.Lock()
   269  	err := write()
   270  	ddw.mu.Unlock()
   271  	return err
   272  }