github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/framework/internal/eventloop/runner_test.go (about) 1 // Copyright 2022 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package eventloop 15 16 import ( 17 "context" 18 "sync" 19 "testing" 20 "time" 21 22 runtime "github.com/pingcap/tiflow/engine/executor/worker" 23 "github.com/pingcap/tiflow/pkg/errors" 24 "github.com/stretchr/testify/mock" 25 "github.com/stretchr/testify/require" 26 "go.uber.org/atomic" 27 ) 28 29 type toyTaskStatus = int32 30 31 const ( 32 toyTaskUninit = toyTaskStatus(iota + 1) 33 toyTaskRunning 34 toyTaskClosing 35 toyTaskClosed 36 toyTaskCanceled 37 ) 38 39 type toyTask struct { 40 mock.Mock 41 42 t *testing.T 43 status atomic.Int32 44 45 expectForcefulExit bool 46 injectedErrCh chan error 47 } 48 49 func newToyTask(t *testing.T, willExitForcefully bool) *toyTask { 50 return &toyTask{ 51 t: t, 52 status: *atomic.NewInt32(toyTaskUninit), 53 expectForcefulExit: willExitForcefully, 54 injectedErrCh: make(chan error, 1), 55 } 56 } 57 58 func (t *toyTask) Init(ctx context.Context) error { 59 require.True(t.t, t.status.CAS(toyTaskUninit, toyTaskRunning)) 60 61 args := t.Called(ctx) 62 return args.Error(0) 63 } 64 65 func (t *toyTask) Poll(ctx context.Context) error { 66 require.Equal(t.t, toyTaskRunning, t.status.Load()) 67 68 select { 69 case err := <-t.injectedErrCh: 70 return err 71 case <-ctx.Done(): 72 return ctx.Err() 73 default: 74 return nil 75 } 76 } 77 78 func (t *toyTask) Stop(ctx context.Context) error { 79 require.True(t.t, t.status.CAS(toyTaskRunning, toyTaskCanceled)) 80 args := t.Called(ctx) 81 return args.Error(0) 82 } 83 84 func (t *toyTask) NotifyExit(ctx context.Context, errIn error) error { 85 if !errors.Is(errIn, errors.ErrWorkerCancel) { 86 require.True(t.t, t.status.CAS(toyTaskRunning, toyTaskClosing)) 87 } 88 89 args := t.Called(ctx, errIn) 90 return args.Error(0) 91 } 92 93 func (t *toyTask) Close(ctx context.Context) error { 94 if !t.expectForcefulExit { 95 require.True(t.t, t.status.CAS(toyTaskClosing, toyTaskClosed)) 96 } else { 97 require.True(t.t, t.status.CAS(toyTaskRunning, toyTaskClosed)) 98 } 99 100 args := t.Called(ctx) 101 return args.Error(0) 102 } 103 104 func (t *toyTask) ID() runtime.RunnableID { 105 return "toy" 106 } 107 108 func TestRunnerNormalPath(t *testing.T) { 109 t.Parallel() 110 111 task := newToyTask(t, false) 112 runner := NewRunner(task) 113 114 errIn := errors.New("injected error") 115 116 task.On("Init", mock.Anything).Return(nil).Once() 117 task.On("NotifyExit", mock.Anything, errIn).Return(nil).Once() 118 task.On("Close", mock.Anything).Return(nil).Once() 119 120 var wg sync.WaitGroup 121 wg.Add(1) 122 go func() { 123 defer wg.Done() 124 125 err := runner.Run(context.Background()) 126 require.Error(t, err) 127 require.Regexp(t, "injected error", err) 128 }() 129 130 require.Eventually(t, func() bool { 131 return task.status.Load() == toyTaskRunning 132 }, 1*time.Second, 10*time.Millisecond) 133 134 task.injectedErrCh <- errIn 135 136 require.Eventually(t, func() bool { 137 return task.status.Load() == toyTaskClosed 138 }, 1*time.Second, 10*time.Millisecond) 139 140 wg.Wait() 141 task.AssertExpectations(t) 142 } 143 144 func TestRunnerForcefulExit(t *testing.T) { 145 t.Parallel() 146 147 task := newToyTask(t, true) 148 runner := NewRunner(task) 149 150 errIn := errors.ErrWorkerSuicide.GenWithStackByArgs() 151 152 task.On("Init", mock.Anything).Return(nil).Once() 153 task.On("Close", mock.Anything).Return(nil).Once() 154 155 var wg sync.WaitGroup 156 wg.Add(1) 157 go func() { 158 defer wg.Done() 159 160 err := runner.Run(context.Background()) 161 require.Error(t, err) 162 require.Regexp(t, "ErrWorkerSuicide", err) 163 }() 164 165 require.Eventually(t, func() bool { 166 return task.status.Load() == toyTaskRunning 167 }, 1*time.Second, 10*time.Millisecond) 168 169 task.injectedErrCh <- errIn 170 171 require.Eventually(t, func() bool { 172 return task.status.Load() == toyTaskClosed 173 }, 1*time.Second, 10*time.Millisecond) 174 175 wg.Wait() 176 task.AssertExpectations(t) 177 } 178 179 func TestRunnerContextCanceled(t *testing.T) { 180 t.Parallel() 181 182 task := newToyTask(t, true) 183 runner := NewRunner(task) 184 185 task.On("Init", mock.Anything).Return(nil).Once() 186 task.On("Close", mock.Anything).Return(nil).Once() 187 188 ctx, cancel := context.WithCancel(context.Background()) 189 go func() { 190 time.Sleep(100 * time.Millisecond) 191 cancel() 192 }() 193 194 err := runner.Run(ctx) 195 require.Error(t, err) 196 require.Regexp(t, "context canceled", err) 197 } 198 199 func TestRunnerStopByCancel(t *testing.T) { 200 t.Parallel() 201 202 task := newToyTask(t, true) 203 runner := NewRunner(task) 204 errIn := errors.ErrWorkerCancel.GenWithStackByArgs() 205 206 task.On("Init", mock.Anything).Return(nil).Once() 207 task.On("NotifyExit", mock.Anything, errIn).Return(nil).Once() 208 task.On("Stop", mock.Anything).Return(nil).Once() 209 210 var wg sync.WaitGroup 211 wg.Add(1) 212 go func() { 213 defer wg.Done() 214 err := runner.Run(context.Background()) 215 require.Error(t, err) 216 require.Regexp(t, "worker is canceled", err) 217 }() 218 219 require.Eventually(t, func() bool { 220 return task.status.Load() == toyTaskRunning 221 }, 1*time.Second, 10*time.Millisecond) 222 223 // Inject canceled worker and check runner.Stop is called 224 task.injectedErrCh <- errIn 225 require.Eventually(t, func() bool { 226 return task.status.Load() == toyTaskCanceled 227 }, 1*time.Second, 10*time.Millisecond) 228 229 wg.Wait() 230 task.AssertExpectations(t) 231 }