github.com/koko1123/flow-go-1@v0.29.6/engine/execution/ingestion/uploader/uploader_test.go (about)

     1  package uploader
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"runtime/debug"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/rs/zerolog"
    12  	"github.com/stretchr/testify/require"
    13  	"go.uber.org/atomic"
    14  
    15  	"github.com/koko1123/flow-go-1/engine/execution"
    16  	"github.com/koko1123/flow-go-1/engine/execution/state/unittest"
    17  	"github.com/koko1123/flow-go-1/module/metrics"
    18  	testutils "github.com/koko1123/flow-go-1/utils/unittest"
    19  	unittest2 "github.com/koko1123/flow-go-1/utils/unittest"
    20  )
    21  
    22  func Test_AsyncUploader(t *testing.T) {
    23  
    24  	computationResult := unittest.ComputationResultFixture(nil)
    25  
    26  	t.Run("uploads are run in parallel and emit metrics", func(t *testing.T) {
    27  		wgUploadStarted := sync.WaitGroup{}
    28  		wgUploadStarted.Add(3)
    29  
    30  		wgContinueUpload := sync.WaitGroup{}
    31  		wgContinueUpload.Add(1)
    32  
    33  		uploader := &DummyUploader{
    34  			f: func() error {
    35  				// this should be called 3 times
    36  				wgUploadStarted.Done()
    37  
    38  				wgContinueUpload.Wait()
    39  
    40  				return nil
    41  			},
    42  		}
    43  
    44  		metrics := &DummyCollector{}
    45  		async := NewAsyncUploader(uploader, 1*time.Nanosecond, 1, zerolog.Nop(), metrics)
    46  
    47  		err := async.Upload(computationResult)
    48  		require.NoError(t, err)
    49  
    50  		err = async.Upload(computationResult)
    51  		require.NoError(t, err)
    52  
    53  		err = async.Upload(computationResult)
    54  		require.NoError(t, err)
    55  
    56  		wgUploadStarted.Wait() // all three are in progress, check metrics
    57  
    58  		require.Equal(t, int64(3), metrics.Counter.Load())
    59  
    60  		wgContinueUpload.Done() //release all
    61  
    62  		// shut down component
    63  		<-async.Done()
    64  
    65  		require.Equal(t, int64(0), metrics.Counter.Load())
    66  		require.True(t, metrics.DurationTotal.Load() > 0, "duration should be nonzero")
    67  	})
    68  
    69  	t.Run("failed uploads are retried", func(t *testing.T) {
    70  
    71  		callCount := 0
    72  
    73  		wg := sync.WaitGroup{}
    74  		wg.Add(1)
    75  
    76  		uploader := &DummyUploader{
    77  			f: func() error {
    78  				// force an upload error to test that upload is retried 3 times
    79  				if callCount < 3 {
    80  					callCount++
    81  					return fmt.Errorf("artificial upload error")
    82  				}
    83  				wg.Done()
    84  				return nil
    85  			},
    86  		}
    87  
    88  		async := NewAsyncUploader(uploader, 1*time.Nanosecond, 5, zerolog.Nop(), &metrics.NoopCollector{})
    89  
    90  		err := async.Upload(computationResult)
    91  		require.NoError(t, err)
    92  
    93  		wg.Wait()
    94  
    95  		require.Equal(t, 3, callCount)
    96  	})
    97  
    98  	// This test shuts down the async uploader right after the upload has started. The upload has an error to force
    99  	// the retry mechanism to kick in (under normal circumstances). Since the component is shutting down, the retry
   100  	// should not kick in.
   101  	//
   102  	// sequence of events:
   103  	// 1. create async uploader and initiate upload with an error - to force retrying
   104  	// 2. shut down async uploader right after upload initiated (not completed)
   105  	// 3. assert that upload called only once even when trying to use retry mechanism
   106  	t.Run("stopping component stops retrying", func(t *testing.T) {
   107  		testutils.SkipUnless(t, testutils.TEST_FLAKY, "flaky")
   108  
   109  		callCount := 0
   110  		t.Log("test started grID:", string(bytes.Fields(debug.Stack())[1]))
   111  
   112  		// this wait group ensures that async uploader has a chance to start the upload before component is shut down
   113  		// otherwise, there's a race condition that can happen where the component can shut down before the async uploader
   114  		// has a chance to start the upload
   115  		wgUploadStarted := sync.WaitGroup{}
   116  		wgUploadStarted.Add(1)
   117  
   118  		// this wait group ensures that async uploader won't send an error (to test if retry will kick in) until
   119  		// the component has initiated shutting down (which should stop retry from working)
   120  		wgShutdownStarted := sync.WaitGroup{}
   121  		wgShutdownStarted.Add(1)
   122  		t.Log("added 1 to wait group grID:", string(bytes.Fields(debug.Stack())[1]))
   123  
   124  		uploader := &DummyUploader{
   125  			f: func() error {
   126  				t.Log("DummyUploader func() - about to call wgUploadStarted.Done() grID:", string(bytes.Fields(debug.Stack())[1]))
   127  				// signal to main goroutine that upload started, so it can initiate shutting down component
   128  				wgUploadStarted.Done()
   129  
   130  				t.Log("DummyUpload func() waiting for component shutdown to start grID:", string(bytes.Fields(debug.Stack())[1]))
   131  				wgShutdownStarted.Wait()
   132  				t.Log("DummyUploader func() component shutdown started, about to return error grID:", string(bytes.Fields(debug.Stack())[1]))
   133  
   134  				// force an upload error to test that upload is never retried (because component is shut down)
   135  				// normally, we would see retry mechanism kick in and the callCount would be > 1
   136  				// but since component has started shutting down, we expect callCount to be 1
   137  				// In summary, callCount SHOULD be called only once - but we want the test to TRY and call it more than once to prove that it
   138  				// was only called it once. If we changed it to 'callCount < 1' that wouldn't prove that the test tried to call it more than once
   139  				// and wouldn't prove that stopping the component stopped the retry mechanism.
   140  				if callCount < 5 {
   141  					t.Logf("DummyUploader func() incrementing callCount=%d grID: %s", callCount, string(bytes.Fields(debug.Stack())[1]))
   142  					callCount++
   143  					t.Logf("DummyUploader func() about to return error callCount=%d grID: %s", callCount, string(bytes.Fields(debug.Stack())[1]))
   144  					return fmt.Errorf("this should return only once")
   145  				}
   146  				return nil
   147  			},
   148  		}
   149  		t.Log("about to create NewAsyncUploader grID:", string(bytes.Fields(debug.Stack())[1]))
   150  		async := NewAsyncUploader(uploader, 1*time.Nanosecond, 5, zerolog.Nop(), &metrics.NoopCollector{})
   151  		t.Log("about to call async.Upload() grID:", string(bytes.Fields(debug.Stack())[1]))
   152  		err := async.Upload(computationResult) // doesn't matter what we upload
   153  		require.NoError(t, err)
   154  
   155  		// stop component and check that it's fully stopped
   156  		t.Log("about to close async uploader grID:", string(bytes.Fields(debug.Stack())[1]))
   157  
   158  		// wait until upload has started before shutting down the component
   159  		wgUploadStarted.Wait()
   160  
   161  		// stop component and check that it's fully stopped
   162  		t.Log("about to initiate shutdown grID: ", string(bytes.Fields(debug.Stack())[1]))
   163  		c := async.Done()
   164  		t.Log("about to notify upload() that shutdown started and can continue uploading grID:", string(bytes.Fields(debug.Stack())[1]))
   165  		wgShutdownStarted.Done()
   166  		t.Log("about to check async done channel is closed grID:", string(bytes.Fields(debug.Stack())[1]))
   167  		unittest2.RequireCloseBefore(t, c, 1*time.Second, "async uploader not closed in time")
   168  
   169  		t.Log("about to check if callCount is 1 grID:", string(bytes.Fields(debug.Stack())[1]))
   170  		require.Equal(t, 1, callCount)
   171  	})
   172  
   173  	t.Run("onComplete callback called if set", func(t *testing.T) {
   174  		var onCompleteCallbackCalled = false
   175  
   176  		wgUploadCalleded := sync.WaitGroup{}
   177  		wgUploadCalleded.Add(1)
   178  
   179  		uploader := &DummyUploader{
   180  			f: func() error {
   181  				wgUploadCalleded.Done()
   182  				return nil
   183  			},
   184  		}
   185  
   186  		async := NewAsyncUploader(uploader, 1*time.Nanosecond, 1, zerolog.Nop(), &DummyCollector{})
   187  		async.SetOnCompleteCallback(func(computationResult *execution.ComputationResult, err error) {
   188  			onCompleteCallbackCalled = true
   189  		})
   190  
   191  		err := async.Upload(computationResult)
   192  		require.NoError(t, err)
   193  
   194  		wgUploadCalleded.Wait()
   195  		<-async.Done()
   196  
   197  		require.True(t, onCompleteCallbackCalled)
   198  	})
   199  }
   200  
   201  // DummyUploader is an Uploader implementation with an Upload() callback
   202  type DummyUploader struct {
   203  	f func() error
   204  }
   205  
   206  func (d *DummyUploader) Upload(_ *execution.ComputationResult) error {
   207  	return d.f()
   208  }
   209  
   210  // FailingUploader mocks upload failure cases
   211  type FailingUploader struct {
   212  	failTimes int
   213  	callCount int
   214  }
   215  
   216  func (d *FailingUploader) Upload(_ *execution.ComputationResult) error {
   217  	defer func() {
   218  		d.callCount++
   219  	}()
   220  
   221  	if d.callCount <= d.failTimes {
   222  		return fmt.Errorf("an artificial error")
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  // DummyCollector is test uploader metrics implementation
   229  type DummyCollector struct {
   230  	metrics.NoopCollector
   231  	Counter       atomic.Int64
   232  	DurationTotal atomic.Int64
   233  }
   234  
   235  func (d *DummyCollector) ExecutionBlockDataUploadStarted() {
   236  	d.Counter.Inc()
   237  }
   238  
   239  func (d *DummyCollector) ExecutionBlockDataUploadFinished(dur time.Duration) {
   240  	d.Counter.Dec()
   241  	d.DurationTotal.Add(dur.Nanoseconds())
   242  }