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 }