github.com/uber/kraken@v0.1.4/lib/torrent/storage/agentstorage/torrent.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package agentstorage
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  
    22  	"github.com/uber/kraken/core"
    23  	"github.com/uber/kraken/lib/store"
    24  	"github.com/uber/kraken/lib/torrent/storage"
    25  	"github.com/uber/kraken/lib/torrent/storage/piecereader"
    26  	"github.com/uber/kraken/utils/log"
    27  
    28  	"github.com/willf/bitset"
    29  	"go.uber.org/atomic"
    30  )
    31  
    32  var (
    33  	errPieceNotComplete   = errors.New("piece not complete")
    34  	errWritePieceConflict = errors.New("piece is already being written to")
    35  )
    36  
    37  // caDownloadStore defines the CADownloadStore methods which Torrent requires. Useful
    38  // for testing purposes, where we need to mock certain methods.
    39  type caDownloadStore interface {
    40  	MoveDownloadFileToCache(name string) error
    41  	GetDownloadFileReadWriter(name string) (store.FileReadWriter, error)
    42  	Any() *store.CADownloadStoreScope
    43  	Download() *store.CADownloadStoreScope
    44  	InCacheError(error) bool
    45  }
    46  
    47  // Torrent implements a Torrent on top of an AgentFileStore.
    48  // It Allows concurrent writes on distinct pieces, and concurrent reads on all
    49  // pieces. Behavior is undefined if multiple Torrent instances are backed
    50  // by the same file store and metainfo.
    51  type Torrent struct {
    52  	metaInfo    *core.MetaInfo
    53  	cads        caDownloadStore
    54  	pieces      []*piece
    55  	numComplete *atomic.Int32
    56  	committed   *atomic.Bool
    57  }
    58  
    59  // NewTorrent creates a new Torrent.
    60  func NewTorrent(cads caDownloadStore, mi *core.MetaInfo) (*Torrent, error) {
    61  	pieces, numComplete, err := restorePieces(mi.Digest(), cads, mi.NumPieces())
    62  	if err != nil {
    63  		return nil, fmt.Errorf("restore pieces: %s", err)
    64  	}
    65  
    66  	committed := false
    67  	if numComplete == len(pieces) {
    68  		if err := cads.MoveDownloadFileToCache(mi.Digest().Hex()); err != nil && !os.IsExist(err) {
    69  			return nil, fmt.Errorf("move file to cache: %s", err)
    70  		}
    71  		committed = true
    72  	}
    73  
    74  	return &Torrent{
    75  		cads:        cads,
    76  		metaInfo:    mi,
    77  		pieces:      pieces,
    78  		numComplete: atomic.NewInt32(int32(numComplete)),
    79  		committed:   atomic.NewBool(committed),
    80  	}, nil
    81  }
    82  
    83  // Digest returns the digest of the target blob.
    84  func (t *Torrent) Digest() core.Digest {
    85  	return t.metaInfo.Digest()
    86  }
    87  
    88  // Stat returns the storage.TorrentInfo for t.
    89  func (t *Torrent) Stat() *storage.TorrentInfo {
    90  	return storage.NewTorrentInfo(t.metaInfo, t.Bitfield())
    91  }
    92  
    93  // InfoHash returns the torrent metainfo hash.
    94  func (t *Torrent) InfoHash() core.InfoHash {
    95  	return t.metaInfo.InfoHash()
    96  }
    97  
    98  // NumPieces returns the number of pieces in the torrent.
    99  func (t *Torrent) NumPieces() int {
   100  	return len(t.pieces)
   101  }
   102  
   103  // Length returns the length of the target file.
   104  func (t *Torrent) Length() int64 {
   105  	return t.metaInfo.Length()
   106  }
   107  
   108  // PieceLength returns the length of piece pi.
   109  func (t *Torrent) PieceLength(pi int) int64 {
   110  	return t.metaInfo.GetPieceLength(pi)
   111  }
   112  
   113  // MaxPieceLength returns the longest piece length of the torrent.
   114  func (t *Torrent) MaxPieceLength() int64 {
   115  	return t.PieceLength(0)
   116  }
   117  
   118  // Complete indicates whether the torrent is complete or not. Completeness is
   119  // defined by whether the torrent file has been committed to the cache directory.
   120  func (t *Torrent) Complete() bool {
   121  	return t.committed.Load()
   122  }
   123  
   124  // BytesDownloaded returns an estimate of the number of bytes downloaded in the
   125  // torrent.
   126  func (t *Torrent) BytesDownloaded() int64 {
   127  	return min(int64(t.numComplete.Load())*t.metaInfo.PieceLength(), t.metaInfo.Length())
   128  }
   129  
   130  // Bitfield returns the bitfield of pieces where true denotes a complete piece
   131  // and false denotes an incomplete piece.
   132  func (t *Torrent) Bitfield() *bitset.BitSet {
   133  	bitfield := bitset.New(uint(len(t.pieces)))
   134  	for i, p := range t.pieces {
   135  		if p.complete() {
   136  			bitfield.Set(uint(i))
   137  		}
   138  	}
   139  	return bitfield
   140  }
   141  
   142  func (t *Torrent) String() string {
   143  	downloaded := int(float64(t.BytesDownloaded()) / float64(t.metaInfo.Length()) * 100)
   144  	return fmt.Sprintf(
   145  		"torrent(name=%s, hash=%s, downloaded=%d%%)",
   146  		t.Digest().Hex(), t.InfoHash().Hex(), downloaded)
   147  }
   148  
   149  func (t *Torrent) getPiece(pi int) (*piece, error) {
   150  	if pi >= len(t.pieces) {
   151  		return nil, fmt.Errorf("invalid piece index %d: num pieces = %d", pi, len(t.pieces))
   152  	}
   153  	return t.pieces[pi], nil
   154  }
   155  
   156  // markPieceComplete must only be called once per piece.
   157  func (t *Torrent) markPieceComplete(pi int) error {
   158  	updated, err := t.cads.Download().SetMetadataAt(
   159  		t.Digest().Hex(), &pieceStatusMetadata{}, []byte{byte(_complete)}, int64(pi))
   160  	if err != nil {
   161  		return fmt.Errorf("write piece metadata: %s", err)
   162  	}
   163  	if !updated {
   164  		// This could mean there's another thread with a Torrent instance using
   165  		// the same file as us.
   166  		log.Errorf(
   167  			"Invariant violation: piece marked complete twice: piece %d in %s", pi, t.Digest().Hex())
   168  	}
   169  	t.pieces[pi].markComplete()
   170  	t.numComplete.Inc()
   171  	return nil
   172  }
   173  
   174  // writePiece writes data to piece pi. If the write succeeds, marks the piece as completed.
   175  func (t *Torrent) writePiece(src storage.PieceReader, pi int) error {
   176  	f, err := t.cads.GetDownloadFileReadWriter(t.metaInfo.Digest().Hex())
   177  	if err != nil {
   178  		return fmt.Errorf("get download writer: %s", err)
   179  	}
   180  	defer f.Close()
   181  
   182  	h := core.PieceHash()
   183  	r := io.TeeReader(src, h) // Calculates piece sum as we write to file.
   184  
   185  	if _, err := f.Seek(t.getFileOffset(pi), 0); err != nil {
   186  		return fmt.Errorf("seek: %s", err)
   187  	}
   188  	if _, err := io.Copy(f, r); err != nil {
   189  		return fmt.Errorf("copy: %s", err)
   190  	}
   191  	if h.Sum32() != t.metaInfo.GetPieceSum(pi) {
   192  		return errors.New("invalid piece sum")
   193  	}
   194  
   195  	if err := t.markPieceComplete(pi); err != nil {
   196  		return fmt.Errorf("mark piece complete: %s", err)
   197  	}
   198  	return nil
   199  }
   200  
   201  // WritePiece writes data to piece pi.
   202  func (t *Torrent) WritePiece(src storage.PieceReader, pi int) error {
   203  	piece, err := t.getPiece(pi)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	if int64(src.Length()) != t.PieceLength(pi) {
   208  		return fmt.Errorf(
   209  			"invalid piece length: expected %d, got %d", t.PieceLength(pi), src.Length())
   210  	}
   211  
   212  	// Exit quickly if the piece is not writable.
   213  	if piece.complete() {
   214  		return storage.ErrPieceComplete
   215  	}
   216  	if piece.dirty() {
   217  		return errWritePieceConflict
   218  	}
   219  
   220  	dirty, complete := piece.tryMarkDirty()
   221  	if dirty {
   222  		return errWritePieceConflict
   223  	} else if complete {
   224  		return storage.ErrPieceComplete
   225  	}
   226  
   227  	// At this point, we've determined that the piece is not complete and ensured
   228  	// we are the only thread which may write the piece. We do not block other
   229  	// threads from checking if the piece is writable.
   230  
   231  	if err := t.writePiece(src, pi); err != nil {
   232  		// Allow other threads to write this piece since we mysteriously failed.
   233  		piece.markEmpty()
   234  		return fmt.Errorf("write piece: %s", err)
   235  	}
   236  
   237  	if int(t.numComplete.Load()) == len(t.pieces) {
   238  		// Multiple threads may attempt to move the download file to cache, however
   239  		// only one will succeed while the others will receive (and ignore) file exist
   240  		// error.
   241  		err := t.cads.MoveDownloadFileToCache(t.metaInfo.Digest().Hex())
   242  		if err != nil && !os.IsExist(err) {
   243  			return fmt.Errorf("download completed but failed to move file to cache directory: %s", err)
   244  		}
   245  		t.committed.Store(true)
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  type opener struct {
   252  	torrent *Torrent
   253  }
   254  
   255  func (o *opener) Open() (store.FileReader, error) {
   256  	return o.torrent.cads.Any().GetFileReader(o.torrent.Digest().Hex())
   257  }
   258  
   259  // GetPieceReader returns a reader for piece pi.
   260  func (t *Torrent) GetPieceReader(pi int) (storage.PieceReader, error) {
   261  	piece, err := t.getPiece(pi)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	if !piece.complete() {
   266  		return nil, errPieceNotComplete
   267  	}
   268  	return piecereader.NewFileReader(t.getFileOffset(pi), t.PieceLength(pi), &opener{t}), nil
   269  }
   270  
   271  // HasPiece returns if piece pi is complete.
   272  func (t *Torrent) HasPiece(pi int) bool {
   273  	piece, err := t.getPiece(pi)
   274  	if err != nil {
   275  		return false
   276  	}
   277  	return piece.complete()
   278  }
   279  
   280  // MissingPieces returns the indeces of all missing pieces.
   281  func (t *Torrent) MissingPieces() []int {
   282  	var missing []int
   283  	for i, p := range t.pieces {
   284  		if !p.complete() {
   285  			missing = append(missing, i)
   286  		}
   287  	}
   288  	return missing
   289  }
   290  
   291  // getFileOffset calculates the offset in the torrent file given piece index.
   292  // Assumes pi is a valid piece index.
   293  func (t *Torrent) getFileOffset(pi int) int64 {
   294  	return t.metaInfo.PieceLength() * int64(pi)
   295  }
   296  
   297  func min(a, b int64) int64 {
   298  	if a < b {
   299  		return a
   300  	}
   301  	return b
   302  }