storj.io/uplink@v1.13.0/private/storage/streams/segmenttracker/tracker_test.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  	"errors"
     9  	"fmt"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"storj.io/uplink/private/metaclient"
    16  )
    17  
    18  func TestTracker(t *testing.T) {
    19  	setup := func(eTag string) (*Tracker, *fakeBatchScheduler) {
    20  		eTagCh := make(chan []byte, 1)
    21  		eTagCh <- []byte(eTag)
    22  		scheduler := new(fakeBatchScheduler)
    23  		return New(scheduler, eTagCh), scheduler
    24  	}
    25  
    26  	segment := func(index int32) Segment {
    27  		return &fakeSegment{index: index}
    28  	}
    29  
    30  	badSegment := func(index int32) Segment {
    31  		return &fakeSegment{index: index, err: errors.New("oh no")}
    32  	}
    33  
    34  	makeInlineSegmentWithEncryptedTag := func(index int32, encryptedTag string) metaclient.BatchItem {
    35  		return &metaclient.MakeInlineSegmentParams{PlainSize: int64(index), EncryptedTag: []byte(encryptedTag)}
    36  	}
    37  
    38  	makeInlineSegment := func(index int32) metaclient.BatchItem {
    39  		return makeInlineSegmentWithEncryptedTag(index, "")
    40  	}
    41  
    42  	commitSegmentWithEncryptedTag := func(index int32, encryptedTag string) metaclient.BatchItem {
    43  		return &metaclient.CommitSegmentParams{PlainSize: int64(index), EncryptedTag: []byte(encryptedTag)}
    44  	}
    45  
    46  	commitSegment := func(index int32) metaclient.BatchItem {
    47  		return commitSegmentWithEncryptedTag(index, "")
    48  	}
    49  
    50  	t.Run("SegmentDone holds back highest seen segment", func(t *testing.T) {
    51  		tracker, scheduler := setup("")
    52  
    53  		// 2 will should be held back
    54  		tracker.SegmentDone(segment(2), makeInlineSegment(2))
    55  		scheduler.AssertScheduledAndReset(t)
    56  
    57  		// 3 done will allow 1 to be scheduled
    58  		tracker.SegmentDone(segment(3), makeInlineSegment(3))
    59  		scheduler.AssertScheduledAndReset(t, makeInlineSegment(2))
    60  
    61  		// 1 will be scheduled immediate since 3 is known and higher
    62  		tracker.SegmentDone(segment(1), makeInlineSegment(1))
    63  		scheduler.AssertScheduledAndReset(t, makeInlineSegment(1))
    64  
    65  		// 4 will still be held back (and 3 scheduled) even though 5 is
    66  		// known to be the last segment.
    67  		tracker.SegmentsScheduled(segment(5))
    68  		tracker.SegmentDone(segment(4), makeInlineSegment(4))
    69  		scheduler.AssertScheduledAndReset(t, makeInlineSegment(3))
    70  	})
    71  
    72  	t.Run("SegmentDone immediately schedules when etag is not a concern", func(t *testing.T) {
    73  		scheduler := new(fakeBatchScheduler)
    74  		tracker := New(scheduler, nil)
    75  
    76  		tracker.SegmentDone(segment(1), makeInlineSegment(1))
    77  		scheduler.AssertScheduledAndReset(t, makeInlineSegment(1))
    78  	})
    79  
    80  	t.Run("Flush flushes the last segment", func(t *testing.T) {
    81  		t.Run("MakeInlineSegment", func(t *testing.T) {
    82  			tracker, scheduler := setup("etag")
    83  			tracker.SegmentDone(segment(1), makeInlineSegment(1))
    84  			tracker.SegmentsScheduled(segment(1))
    85  
    86  			scheduler.AssertScheduledAndReset(t)
    87  			err := tracker.Flush(context.Background())
    88  			require.NoError(t, err)
    89  			scheduler.AssertScheduledAndReset(t, makeInlineSegmentWithEncryptedTag(1, "etag-1"))
    90  		})
    91  		t.Run("CommitSegment", func(t *testing.T) {
    92  			tracker, scheduler := setup("etag")
    93  			tracker.SegmentDone(segment(1), commitSegment(1))
    94  			tracker.SegmentsScheduled(segment(1))
    95  
    96  			scheduler.AssertScheduledAndReset(t)
    97  			err := tracker.Flush(context.Background())
    98  			require.NoError(t, err)
    99  			scheduler.AssertScheduledAndReset(t, commitSegmentWithEncryptedTag(1, "etag-1"))
   100  		})
   101  	})
   102  
   103  	t.Run("Flush responds to context cancellation waiting for etag", func(t *testing.T) {
   104  		scheduler := new(fakeBatchScheduler)
   105  
   106  		tracker := New(scheduler, make(chan []byte))
   107  		tracker.SegmentDone(segment(1), makeInlineSegment(1))
   108  		tracker.SegmentsScheduled(segment(1))
   109  
   110  		ctx, cancel := context.WithCancel(context.Background())
   111  		cancel()
   112  
   113  		err := tracker.Flush(ctx)
   114  		require.Error(t, err)
   115  		require.True(t, errors.Is(err, context.Canceled))
   116  	})
   117  
   118  	t.Run("Flush does not encrypt an empty etag", func(t *testing.T) {
   119  		tracker, scheduler := setup("")
   120  		tracker.SegmentDone(segment(1), makeInlineSegment(1))
   121  		tracker.SegmentsScheduled(segment(1))
   122  
   123  		err := tracker.Flush(context.Background())
   124  		require.NoError(t, err)
   125  		scheduler.AssertScheduledAndReset(t, makeInlineSegment(1))
   126  	})
   127  
   128  	t.Run("Flush fails if last segment was never done", func(t *testing.T) {
   129  		tracker, _ := setup("etag")
   130  		tracker.SegmentDone(segment(1), makeInlineSegment(1))
   131  		tracker.SegmentsScheduled(segment(2))
   132  		err := tracker.Flush(context.Background())
   133  		require.EqualError(t, err, "programmer error: expected held back segment with index 1 to have last segment index 2")
   134  	})
   135  
   136  	t.Run("Flush fails if last segment batch item is unhandled", func(t *testing.T) {
   137  		tracker, _ := setup("etag")
   138  		tracker.SegmentDone(segment(1), &metaclient.BeginSegmentParams{})
   139  		tracker.SegmentsScheduled(segment(1))
   140  		err := tracker.Flush(context.Background())
   141  		require.EqualError(t, err, "unhandled segment batch item type: *metaclient.BeginSegmentParams")
   142  	})
   143  
   144  	t.Run("Flush before SegmentDone is an error", func(t *testing.T) {
   145  		tracker, _ := setup("")
   146  		err := tracker.Flush(context.Background())
   147  		require.EqualError(t, err, "programmer error: no segment has been held back")
   148  	})
   149  
   150  	t.Run("Flush before SegmentsScheduled is an error", func(t *testing.T) {
   151  		tracker, _ := setup("")
   152  		tracker.SegmentDone(segment(1), makeInlineSegment(1))
   153  		err := tracker.Flush(context.Background())
   154  		require.EqualError(t, err, "programmer error: cannot flush before last segment known")
   155  	})
   156  
   157  	t.Run("Flush fails if eTag cannot be encrypted", func(t *testing.T) {
   158  		tracker, _ := setup("etag")
   159  		tracker.SegmentDone(badSegment(1), makeInlineSegment(1))
   160  		tracker.SegmentsScheduled(segment(1))
   161  		err := tracker.Flush(context.Background())
   162  		require.EqualError(t, err, "failed to encrypt eTag: oh no")
   163  	})
   164  
   165  	t.Run("Flush is no-op when etag is not a concern", func(t *testing.T) {
   166  		tracker := New(new(fakeBatchScheduler), nil)
   167  		err := tracker.Flush(context.Background())
   168  		require.NoError(t, err)
   169  	})
   170  }
   171  
   172  type fakeBatchScheduler struct {
   173  	scheduled []metaclient.BatchItem
   174  }
   175  
   176  func (s *fakeBatchScheduler) Schedule(batchItem metaclient.BatchItem) {
   177  	s.scheduled = append(s.scheduled, batchItem)
   178  }
   179  
   180  func (s *fakeBatchScheduler) AssertScheduledAndReset(t *testing.T, expected ...metaclient.BatchItem) {
   181  	assert.Equal(t, expected, s.scheduled)
   182  	s.scheduled = nil
   183  }
   184  
   185  type fakeSegment struct {
   186  	index int32
   187  	err   error
   188  }
   189  
   190  func (s *fakeSegment) Position() metaclient.SegmentPosition {
   191  	return metaclient.SegmentPosition{Index: s.index}
   192  }
   193  
   194  func (s *fakeSegment) EncryptETag(eTag []byte) ([]byte, error) {
   195  	if s.err != nil {
   196  		return nil, s.err
   197  	}
   198  	return []byte(fmt.Sprintf("%s-%d", string(eTag), s.index)), nil
   199  }