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 }