storj.io/uplink@v1.13.0/private/storage/streams/segmenttracker/tracker.go (about)

     1  // Copyright (C) 2023 Storj Labs, Inc.
     2  // See LICENSE for copying information.
     3  
     4  package segmenttracker
     5  
     6  import (
     7  	"context"
     8  	"sync"
     9  
    10  	"github.com/spacemonkeygo/monkit/v3"
    11  	"github.com/zeebo/errs"
    12  
    13  	"storj.io/uplink/private/metaclient"
    14  )
    15  
    16  var mon = monkit.Package()
    17  
    18  // Segment represents a segment being tracked.
    19  type Segment interface {
    20  	Position() metaclient.SegmentPosition
    21  	EncryptETag([]byte) ([]byte, error)
    22  }
    23  
    24  // BatchScheduler schedules batch items to be issued.
    25  type BatchScheduler interface {
    26  	Schedule(batchItem metaclient.BatchItem)
    27  }
    28  
    29  // Tracker tracks segments as they are completed for the purpose of encrypting
    30  // and setting the eTag on the final segment. It hold backs scheduling of the
    31  // last known segment until Flush is called, at which point it verifies that
    32  // the held back segment is indeed the last segment, encrypts the eTag using
    33  // that segment, and injects it into the batch item that commits that segment
    34  // (i.e. MakeInlineSegment or CommitSegment). If the segments are not part of
    35  // a multipart upload (i.e. no eTag to apply), then the tracker schedules
    36  // the segment batch items immediately.
    37  type Tracker struct {
    38  	scheduler BatchScheduler
    39  	eTagCh    <-chan []byte
    40  
    41  	mu sync.Mutex
    42  
    43  	lastIndex *int32
    44  
    45  	heldBackSegment   Segment
    46  	heldBackBatchItem metaclient.BatchItem
    47  }
    48  
    49  // New returns a new tracker that uses the given batch scheduler to schedule
    50  // segment commit items.
    51  func New(scheduler BatchScheduler, eTagCh <-chan []byte) *Tracker {
    52  	return &Tracker{
    53  		scheduler: scheduler,
    54  		eTagCh:    eTagCh,
    55  	}
    56  }
    57  
    58  // SegmentDone notifies the tracker that a segment upload has completed and
    59  // supplies the batchItem that needs to be scheduled to commit the segment ( or
    60  // create the inline segment). If this is the last segment seen so far, it will
    61  // not be scheduled immediately, and will instead be scheduled when a later
    62  // segment finishes or Flush is called. If the tracker was given a nil eTagCh
    63  // channel, then the segment batch item is scheduled immediately.
    64  func (t *Tracker) SegmentDone(segment Segment, batchItem metaclient.BatchItem) {
    65  	// If there will be no eTag to encrypt then there is no reason to gate the
    66  	// scheduling.
    67  	if t.eTagCh == nil {
    68  		t.scheduler.Schedule(batchItem)
    69  		return
    70  	}
    71  
    72  	index := segment.Position().Index
    73  
    74  	t.mu.Lock()
    75  	defer t.mu.Unlock()
    76  
    77  	if t.heldBackSegment != nil {
    78  		// If the segment comes before the held back segment then it can be
    79  		// scheduled immediately since we know it cannot be the last segment.
    80  		if index < t.heldBackSegment.Position().Index {
    81  			t.scheduler.Schedule(batchItem)
    82  			return
    83  		}
    84  		// The held back segment can be scheduled immediately since it comes
    85  		// before this segment and is therefore not the last segment.
    86  		t.scheduler.Schedule(t.heldBackBatchItem)
    87  	}
    88  
    89  	t.heldBackSegment = segment
    90  	t.heldBackBatchItem = batchItem
    91  }
    92  
    93  // SegmentsScheduled is invoked when the last segment upload has been
    94  // scheduled. It allows the tracker to verify that the held back segment is
    95  // actually the last segment when Flush is called.
    96  func (t *Tracker) SegmentsScheduled(lastSegment Segment) {
    97  	t.mu.Lock()
    98  	defer t.mu.Unlock()
    99  	lastIndex := lastSegment.Position().Index
   100  	t.lastIndex = &lastIndex
   101  }
   102  
   103  // Flush schedules the held back segment. It must only be called at least one
   104  // call to SegmentDone as well as SegmentsScheduled. It verifies that the held
   105  // back segment is the last segment (as indicited by SegmentsScheduled). Before
   106  // scheduling the last segment, it reads the eTag from the eTagCh channel
   107  // provided in New, encryptes that eTag with the last segment, and then injects
   108  // the encrypted eTag into the batch item that commits that segment (i.e.
   109  // MakeInlineSegment or CommitSegment).
   110  func (t *Tracker) Flush(ctx context.Context) (err error) {
   111  	defer mon.Task()(&ctx)(&err)
   112  
   113  	if t.eTagCh == nil {
   114  		return nil
   115  	}
   116  
   117  	t.mu.Lock()
   118  	defer t.mu.Unlock()
   119  
   120  	// There should ALWAYS be a held back segment here.
   121  	if t.heldBackSegment == nil {
   122  		return errs.New("programmer error: no segment has been held back")
   123  	}
   124  
   125  	if t.lastIndex == nil {
   126  		return errs.New("programmer error: cannot flush before last segment known")
   127  	}
   128  
   129  	// The held back segment should ALWAYS be the last segment
   130  	if heldBackIndex := t.heldBackSegment.Position().Index; heldBackIndex != *t.lastIndex {
   131  		return errs.New("programmer error: expected held back segment with index %d to have last segment index %d", heldBackIndex, *t.lastIndex)
   132  	}
   133  
   134  	if err := t.addEncryptedETag(ctx, t.heldBackSegment, t.heldBackBatchItem); err != nil {
   135  		return errs.Wrap(err)
   136  	}
   137  
   138  	t.scheduler.Schedule(t.heldBackBatchItem)
   139  	t.heldBackSegment = nil
   140  	t.heldBackBatchItem = nil
   141  	return nil
   142  }
   143  
   144  func (t *Tracker) addEncryptedETag(ctx context.Context, lastSegment Segment, batchItem metaclient.BatchItem) (err error) {
   145  	defer mon.Task()(&ctx)(&err)
   146  
   147  	select {
   148  	case eTag := <-t.eTagCh:
   149  		if len(eTag) == 0 {
   150  			// Either an empty ETag provided by caller or more likely
   151  			// the ETag was not provided before Commit was called.
   152  			return nil
   153  		}
   154  
   155  		encryptedETag, err := lastSegment.EncryptETag(eTag)
   156  		if err != nil {
   157  			return errs.New("failed to encrypt eTag: %w", err)
   158  		}
   159  		switch batchItem := batchItem.(type) {
   160  		case *metaclient.MakeInlineSegmentParams:
   161  			batchItem.EncryptedTag = encryptedETag
   162  		case *metaclient.CommitSegmentParams:
   163  			batchItem.EncryptedTag = encryptedETag
   164  		default:
   165  			return errs.New("unhandled segment batch item type: %T", batchItem)
   166  		}
   167  		return nil
   168  	case <-ctx.Done():
   169  		return ctx.Err()
   170  	}
   171  }