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  }