github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/framework/worker_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 framework
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"sync"
    20  	"testing"
    21  	"time"
    22  
    23  	runtime "github.com/pingcap/tiflow/engine/executor/worker"
    24  	"github.com/pingcap/tiflow/engine/framework/config"
    25  	frameModel "github.com/pingcap/tiflow/engine/framework/model"
    26  	"github.com/pingcap/tiflow/engine/framework/statusutil"
    27  	"github.com/pingcap/tiflow/engine/pkg/clock"
    28  	pkgOrm "github.com/pingcap/tiflow/engine/pkg/orm"
    29  	"github.com/pingcap/tiflow/pkg/errors"
    30  	"github.com/stretchr/testify/mock"
    31  	"github.com/stretchr/testify/require"
    32  	"go.uber.org/atomic"
    33  )
    34  
    35  var _ Worker = (*DefaultBaseWorker)(nil) // _ runtime.Runnable = (Worker)(nil)
    36  
    37  func putMasterMeta(ctx context.Context, t *testing.T, metaclient pkgOrm.Client, metaData *frameModel.MasterMeta) {
    38  	err := metaclient.UpsertJob(ctx, metaData)
    39  	require.NoError(t, err)
    40  }
    41  
    42  func fastMarshalDummyStatus(t *testing.T, val int) []byte {
    43  	dummySt := &dummyStatus{Val: val}
    44  	bytes, err := dummySt.Marshal()
    45  	require.NoError(t, err)
    46  	return bytes
    47  }
    48  
    49  func TestWorkerInitAndClose(t *testing.T) {
    50  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    51  	defer cancel()
    52  
    53  	worker := newMockWorkerImpl(workerID1, masterName)
    54  	worker.clock = clock.NewMock()
    55  	worker.clock.(*clock.Mock).Set(time.Now())
    56  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
    57  		ID:     masterName,
    58  		NodeID: masterNodeName,
    59  		Epoch:  1,
    60  		State:  frameModel.MasterStateInit,
    61  	})
    62  
    63  	worker.On("InitImpl", mock.Anything).Return(nil)
    64  	worker.On("Status").Return(frameModel.WorkerStatus{
    65  		State: frameModel.WorkerStateNormal,
    66  	}, nil)
    67  	worker.On("Tick", mock.Anything).Return(nil)
    68  
    69  	err := worker.Init(ctx)
    70  	require.NoError(t, err)
    71  
    72  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerHeartbeatInterval + 1*time.Second)
    73  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerHeartbeatInterval + 1*time.Second)
    74  
    75  	var hbMsg *frameModel.HeartbeatPingMessage
    76  	require.Eventually(t, func() bool {
    77  		rawMsg, ok := worker.messageSender.TryPop(masterNodeName, frameModel.HeartbeatPingTopic(masterName))
    78  		if ok {
    79  			hbMsg = rawMsg.(*frameModel.HeartbeatPingMessage)
    80  		}
    81  		return ok
    82  	}, time.Second*3, time.Millisecond*10)
    83  	require.Conditionf(t, func() (success bool) {
    84  		return hbMsg.FromWorkerID == workerID1 && hbMsg.Epoch == 1
    85  	}, "unexpected heartbeat %v", hbMsg)
    86  
    87  	err = worker.UpdateStatus(ctx, frameModel.WorkerStatus{State: frameModel.WorkerStateNormal})
    88  	require.NoError(t, err)
    89  
    90  	var statusMsg *statusutil.WorkerStatusMessage
    91  	require.Eventually(t, func() bool {
    92  		err := worker.Poll(ctx)
    93  		require.NoError(t, err)
    94  		rawMsg, ok := worker.messageSender.TryPop(masterNodeName, statusutil.WorkerStatusTopic(masterName))
    95  		if ok {
    96  			statusMsg = rawMsg.(*statusutil.WorkerStatusMessage)
    97  		}
    98  		return !ok
    99  	}, time.Second, time.Millisecond*10)
   100  	checkWorkerStatusMsg(t, &statusutil.WorkerStatusMessage{
   101  		Worker:      workerID1,
   102  		MasterEpoch: 1,
   103  		Status:      &frameModel.WorkerStatus{State: frameModel.WorkerStateNormal},
   104  	}, statusMsg)
   105  
   106  	worker.On("CloseImpl").Return().Once()
   107  	err = worker.Close(ctx)
   108  	require.NoError(t, err)
   109  }
   110  
   111  const (
   112  	heartbeatPingPongTestRepeatTimes = 100
   113  )
   114  
   115  func TestWorkerHeartbeatPingPong(t *testing.T) {
   116  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   117  	defer cancel()
   118  
   119  	worker := newMockWorkerImpl(workerID1, masterName)
   120  	worker.clock = clock.NewMock()
   121  	worker.clock.(*clock.Mock).Set(time.Now())
   122  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   123  		ID:     masterName,
   124  		NodeID: masterNodeName,
   125  		Epoch:  1,
   126  		State:  frameModel.MasterStateInit,
   127  	})
   128  
   129  	worker.On("InitImpl", mock.Anything).Return(nil)
   130  	worker.On("Status").Return(frameModel.WorkerStatus{
   131  		State: frameModel.WorkerStateNormal,
   132  	}, nil)
   133  
   134  	err := worker.Init(ctx)
   135  	require.NoError(t, err)
   136  
   137  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerHeartbeatInterval)
   138  
   139  	worker.On("Tick", mock.Anything).Return(nil)
   140  	var lastHeartbeatSendTime clock.MonotonicTime
   141  	for i := 0; i < heartbeatPingPongTestRepeatTimes; i++ {
   142  		err := worker.Poll(ctx)
   143  		require.NoError(t, err)
   144  
   145  		worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerHeartbeatInterval)
   146  		var hbMsg *frameModel.HeartbeatPingMessage
   147  		require.Eventually(t, func() bool {
   148  			rawMsg, ok := worker.messageSender.TryPop(masterNodeName, frameModel.HeartbeatPingTopic(masterName))
   149  			if ok {
   150  				hbMsg = rawMsg.(*frameModel.HeartbeatPingMessage)
   151  			}
   152  			return ok
   153  		}, time.Second*3, time.Millisecond*10)
   154  
   155  		require.Conditionf(t, func() (success bool) {
   156  			return hbMsg.SendTime.Sub(lastHeartbeatSendTime) >= config.DefaultTimeoutConfig().WorkerHeartbeatInterval &&
   157  				hbMsg.FromWorkerID == workerID1
   158  		}, "last-send-time %s, cur-send-time %s", lastHeartbeatSendTime, hbMsg.SendTime)
   159  		lastHeartbeatSendTime = hbMsg.SendTime
   160  
   161  		pongMsg := &frameModel.HeartbeatPongMessage{
   162  			SendTime:   hbMsg.SendTime,
   163  			ReplyTime:  time.Now(),
   164  			ToWorkerID: workerID1,
   165  			Epoch:      1,
   166  		}
   167  		err = worker.messageHandlerManager.InvokeHandler(
   168  			t, frameModel.HeartbeatPongTopic(masterName, workerID1), masterNodeName, pongMsg)
   169  		require.NoError(t, err)
   170  	}
   171  }
   172  
   173  func TestWorkerMasterFailover(t *testing.T) {
   174  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   175  	defer cancel()
   176  
   177  	worker := newMockWorkerImpl(workerID1, masterName)
   178  	worker.clock = clock.NewMock()
   179  	worker.clock.(*clock.Mock).Set(time.Now())
   180  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   181  		ID:     masterName,
   182  		NodeID: masterNodeName,
   183  		Epoch:  1,
   184  		State:  frameModel.MasterStateInit,
   185  	})
   186  
   187  	worker.On("InitImpl", mock.Anything).Return(nil)
   188  	worker.On("Status").Return(frameModel.WorkerStatus{
   189  		State: frameModel.WorkerStateNormal,
   190  	}, nil)
   191  	err := worker.Init(ctx)
   192  	require.NoError(t, err)
   193  
   194  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerHeartbeatInterval)
   195  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerHeartbeatInterval)
   196  	var hbMsg *frameModel.HeartbeatPingMessage
   197  	require.Eventually(t, func() bool {
   198  		rawMsg, ok := worker.messageSender.TryPop(masterNodeName, frameModel.HeartbeatPingTopic(masterName))
   199  		if ok {
   200  			hbMsg = rawMsg.(*frameModel.HeartbeatPingMessage)
   201  		}
   202  		return ok
   203  	}, time.Second, time.Millisecond*10)
   204  	require.Equal(t, workerID1, hbMsg.FromWorkerID)
   205  
   206  	pongMsg := &frameModel.HeartbeatPongMessage{
   207  		SendTime:   hbMsg.SendTime,
   208  		ReplyTime:  time.Now(),
   209  		ToWorkerID: workerID1,
   210  		Epoch:      1,
   211  	}
   212  	err = worker.messageHandlerManager.InvokeHandler(t,
   213  		frameModel.HeartbeatPongTopic(masterName, workerID1), masterNodeName, pongMsg)
   214  	require.NoError(t, err)
   215  
   216  	worker.clock.(*clock.Mock).Add(time.Second * 1)
   217  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   218  		ID:     masterName,
   219  		NodeID: executorNodeID3,
   220  		Epoch:  2,
   221  		State:  frameModel.MasterStateInit,
   222  	})
   223  
   224  	// Trigger a pull from Meta for the latest master's info.
   225  	worker.clock.(*clock.Mock).Add(3 * config.DefaultTimeoutConfig().WorkerHeartbeatInterval)
   226  
   227  	require.Eventually(t, func() bool {
   228  		return worker.masterClient.MasterNode() == executorNodeID3
   229  	}, time.Second*3, time.Millisecond*10)
   230  }
   231  
   232  func TestWorkerState(t *testing.T) {
   233  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   234  	defer cancel()
   235  
   236  	worker := newMockWorkerImpl(workerID1, masterName)
   237  	worker.clock = clock.NewMock()
   238  	worker.clock.(*clock.Mock).Set(time.Now())
   239  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   240  		ID:     masterName,
   241  		NodeID: masterNodeName,
   242  		Epoch:  1,
   243  		State:  frameModel.MasterStateInit,
   244  	})
   245  
   246  	worker.On("InitImpl", mock.Anything).Return(nil)
   247  	worker.On("Status").Return(frameModel.WorkerStatus{
   248  		State:    frameModel.WorkerStateNormal,
   249  		ExtBytes: fastMarshalDummyStatus(t, 1),
   250  	}, nil)
   251  	worker.On("Tick", mock.Anything).Return(nil)
   252  	worker.On("CloseImpl", mock.Anything).Return()
   253  
   254  	err := worker.Init(ctx)
   255  	require.NoError(t, err)
   256  
   257  	rawStatus, ok := worker.messageSender.TryPop(masterNodeName, statusutil.WorkerStatusTopic(masterName))
   258  	require.True(t, ok)
   259  	msg := rawStatus.(*statusutil.WorkerStatusMessage)
   260  	checkWorkerStatusMsg(t, &statusutil.WorkerStatusMessage{
   261  		Worker:      workerID1,
   262  		MasterEpoch: 1,
   263  		Status: &frameModel.WorkerStatus{
   264  			State: frameModel.WorkerStateInit,
   265  		},
   266  	}, msg)
   267  
   268  	err = worker.UpdateStatus(ctx, frameModel.WorkerStatus{
   269  		State:    frameModel.WorkerStateNormal,
   270  		ExtBytes: fastMarshalDummyStatus(t, 6),
   271  	})
   272  	require.NoError(t, err)
   273  
   274  	rawStatus, ok = worker.messageSender.TryPop(masterNodeName, statusutil.WorkerStatusTopic(masterName))
   275  	require.True(t, ok)
   276  	msg = rawStatus.(*statusutil.WorkerStatusMessage)
   277  	checkWorkerStatusMsg(t, &statusutil.WorkerStatusMessage{
   278  		Worker:      workerID1,
   279  		MasterEpoch: 1,
   280  		Status: &frameModel.WorkerStatus{
   281  			State:    frameModel.WorkerStateNormal,
   282  			ExtBytes: fastMarshalDummyStatus(t, 6),
   283  		},
   284  	}, msg)
   285  
   286  	err = worker.Close(ctx)
   287  	require.NoError(t, err)
   288  }
   289  
   290  func TestWorkerSuicide(t *testing.T) {
   291  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   292  	defer cancel()
   293  
   294  	worker := newMockWorkerImpl(workerID1, masterName)
   295  	worker.clock = clock.NewMock()
   296  	worker.clock.(*clock.Mock).Set(time.Now())
   297  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   298  		ID:     masterName,
   299  		NodeID: masterNodeName,
   300  		Epoch:  1,
   301  		State:  frameModel.MasterStateInit,
   302  	})
   303  
   304  	worker.On("InitImpl", mock.Anything).Return(nil)
   305  	worker.On("Status").Return(frameModel.WorkerStatus{
   306  		State: frameModel.WorkerStateNormal,
   307  	}, nil)
   308  	worker.On("CloseImpl", mock.Anything).Return()
   309  
   310  	err := worker.Init(ctx)
   311  	require.NoError(t, err)
   312  
   313  	worker.On("Tick", mock.Anything).Return(nil)
   314  	err = worker.Poll(ctx)
   315  	require.NoError(t, err)
   316  
   317  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerTimeoutDuration)
   318  	worker.clock.(*clock.Mock).Add(config.DefaultTimeoutConfig().WorkerTimeoutDuration)
   319  
   320  	var exitErr error
   321  	require.Eventually(t, func() bool {
   322  		exitErr = worker.Poll(ctx)
   323  		return exitErr != nil
   324  	}, time.Second*1, time.Millisecond*10)
   325  
   326  	require.Regexp(t, ".*Suicide.*", exitErr.Error())
   327  }
   328  
   329  func TestWorkerSuicideAfterRuntimeDelay(t *testing.T) {
   330  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   331  	defer cancel()
   332  
   333  	submitTime := time.Now()
   334  	worker := newMockWorkerImpl(workerID1, masterName)
   335  	worker.clock = clock.NewMock()
   336  	worker.clock.(*clock.Mock).Set(submitTime.Add(worker.timeoutConfig.WorkerTimeoutDuration * 2))
   337  
   338  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   339  		ID:     masterName,
   340  		NodeID: masterNodeName,
   341  		Epoch:  1,
   342  		State:  frameModel.MasterStateInit,
   343  	})
   344  
   345  	worker.On("InitImpl", mock.Anything).Return(nil)
   346  	worker.On("Status").Return(frameModel.WorkerStatus{
   347  		State: frameModel.WorkerStateNormal,
   348  	}, nil)
   349  	worker.On("Tick", mock.Anything).Return(nil)
   350  	worker.On("CloseImpl", mock.Anything).Return()
   351  
   352  	ctx = runtime.NewRuntimeCtxWithSubmitTime(ctx, clock.ToMono(submitTime))
   353  	err := worker.Init(ctx)
   354  	require.NoError(t, err)
   355  
   356  	time.Sleep(10 * time.Millisecond)
   357  	worker.clock.(*clock.Mock).Add(worker.timeoutConfig.WorkerHeartbeatInterval)
   358  	worker.clock.(*clock.Mock).Add(1 * time.Second)
   359  
   360  	var pollErr error
   361  	require.Eventually(t, func() bool {
   362  		pollErr = worker.Poll(ctx)
   363  		return pollErr != nil
   364  	}, 2*time.Second, 10*time.Millisecond)
   365  	require.Error(t, pollErr)
   366  	require.Regexp(t, ".*Suicide.*", pollErr)
   367  }
   368  
   369  func TestWorkerGracefulExit(t *testing.T) {
   370  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   371  	defer cancel()
   372  
   373  	worker := newMockWorkerImpl(workerID1, masterName)
   374  	worker.clock = clock.NewMock()
   375  	worker.clock.(*clock.Mock).Set(time.Now())
   376  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   377  		ID:     masterName,
   378  		NodeID: masterNodeName,
   379  		Epoch:  1,
   380  		State:  frameModel.MasterStateInit,
   381  	})
   382  
   383  	worker.On("InitImpl", mock.Anything).Return(nil)
   384  
   385  	err := worker.Init(ctx)
   386  	require.NoError(t, err)
   387  
   388  	worker.On("Tick", mock.Anything).
   389  		Return(errors.New("fake error")).Once()
   390  	worker.On("CloseImpl", mock.Anything).Return().Once()
   391  
   392  	err = worker.Poll(ctx)
   393  	require.Error(t, err)
   394  	require.Regexp(t, ".*fake error.*", err)
   395  
   396  	var wg sync.WaitGroup
   397  	wg.Add(1)
   398  	go func() {
   399  		defer wg.Done()
   400  		require.NoError(t, worker.NotifyExit(ctx, err))
   401  	}()
   402  
   403  	for {
   404  		// Make the heartbeat worker tick.
   405  		worker.clock.(*clock.Mock).Add(time.Second)
   406  
   407  		rawMsg, ok := worker.messageSender.TryPop(masterNodeName, frameModel.HeartbeatPingTopic(masterName))
   408  		if !ok {
   409  			continue
   410  		}
   411  		msg := rawMsg.(*frameModel.HeartbeatPingMessage)
   412  		if msg.IsFinished {
   413  			pongMsg := &frameModel.HeartbeatPongMessage{
   414  				SendTime:   msg.SendTime,
   415  				ReplyTime:  time.Now(),
   416  				ToWorkerID: workerID1,
   417  				Epoch:      1,
   418  				IsFinished: true,
   419  			}
   420  
   421  			err := worker.messageHandlerManager.InvokeHandler(
   422  				t,
   423  				frameModel.HeartbeatPongTopic(masterName, workerID1),
   424  				masterNodeName,
   425  				pongMsg,
   426  			)
   427  			require.NoError(t, err)
   428  			break
   429  		}
   430  	}
   431  
   432  	wg.Wait()
   433  	worker.AssertExpectations(t)
   434  }
   435  
   436  func TestWorkerGracefulExitWhileTimeout(t *testing.T) {
   437  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   438  	defer cancel()
   439  
   440  	worker := newMockWorkerImpl(workerID1, masterName)
   441  	worker.clock = clock.NewMock()
   442  	worker.clock.(*clock.Mock).Set(time.Now())
   443  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   444  		ID:     masterName,
   445  		NodeID: masterNodeName,
   446  		Epoch:  1,
   447  		State:  frameModel.MasterStateInit,
   448  	})
   449  
   450  	worker.On("InitImpl", mock.Anything).Return(nil)
   451  
   452  	err := worker.Init(ctx)
   453  	require.NoError(t, err)
   454  
   455  	worker.On("Tick", mock.Anything).
   456  		Return(errors.New("fake error")).Once()
   457  	worker.On("CloseImpl", mock.Anything).Return().Once()
   458  
   459  	err = worker.Poll(ctx)
   460  	require.Error(t, err)
   461  	require.Regexp(t, ".*fake error.*", err)
   462  
   463  	var (
   464  		done atomic.Bool
   465  		wg   sync.WaitGroup
   466  	)
   467  
   468  	wg.Add(1)
   469  	go func() {
   470  		defer wg.Done()
   471  		defer done.Store(true)
   472  		err := worker.NotifyExit(ctx, err)
   473  		require.Error(t, err)
   474  		require.Regexp(t, "context deadline exceeded", err)
   475  	}()
   476  
   477  	for {
   478  		// Make the heartbeat worker tick.
   479  		worker.clock.(*clock.Mock).Add(time.Second)
   480  
   481  		rawMsg, ok := worker.messageSender.TryPop(masterNodeName, frameModel.HeartbeatPingTopic(masterName))
   482  		if !ok {
   483  			continue
   484  		}
   485  		msg := rawMsg.(*frameModel.HeartbeatPingMessage)
   486  		if msg.IsFinished {
   487  			break
   488  		}
   489  	}
   490  
   491  	for !done.Load() {
   492  		worker.clock.(*clock.Mock).Add(time.Second)
   493  		time.Sleep(10 * time.Millisecond)
   494  	}
   495  
   496  	wg.Wait()
   497  }
   498  
   499  func TestCloseBeforeInit(t *testing.T) {
   500  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   501  	defer cancel()
   502  
   503  	worker := newMockWorkerImpl(workerID1, masterName)
   504  
   505  	worker.On("CloseImpl").Return()
   506  	err := worker.Close(ctx)
   507  	require.NoError(t, err)
   508  }
   509  
   510  func TestExitWithoutReturn(t *testing.T) {
   511  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   512  	defer cancel()
   513  
   514  	worker := newMockWorkerImpl(workerID1, masterName)
   515  	worker.clock = clock.NewMock()
   516  	worker.clock.(*clock.Mock).Set(time.Now())
   517  	putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   518  		ID:     masterName,
   519  		NodeID: masterNodeName,
   520  		Epoch:  1,
   521  		State:  frameModel.MasterStateInit,
   522  	})
   523  
   524  	worker.On("InitImpl", mock.Anything).Return(nil)
   525  	worker.On("Status").Return(frameModel.WorkerStatus{
   526  		State: frameModel.WorkerStateNormal,
   527  	}, nil)
   528  
   529  	err := worker.Init(ctx)
   530  	require.NoError(t, err)
   531  
   532  	worker.On("Tick", mock.Anything).Return(nil)
   533  	worker.On("CloseImpl", mock.Anything).Return().Once()
   534  
   535  	_ = worker.DefaultBaseWorker.Exit(ctx, ExitReasonFailed, errors.New("Exit error"), nil)
   536  
   537  	err = worker.Poll(ctx)
   538  	require.Error(t, err)
   539  	require.Regexp(t, "Exit error", err)
   540  }
   541  
   542  func checkWorkerStatusMsg(t *testing.T, expect, msg *statusutil.WorkerStatusMessage) {
   543  	require.Equal(t, expect.Worker, msg.Worker)
   544  	require.Equal(t, expect.MasterEpoch, msg.MasterEpoch)
   545  	require.Equal(t, expect.Status.State, expect.Status.State)
   546  	require.Equal(t, expect.Status.ErrorMsg, expect.Status.ErrorMsg)
   547  	require.Equal(t, expect.Status.ExtBytes, expect.Status.ExtBytes)
   548  }
   549  
   550  func TestWorkerExit(t *testing.T) {
   551  	cases := []struct {
   552  		exitReason       ExitReason
   553  		err              error
   554  		extMsg           []byte
   555  		expectedState    frameModel.WorkerState
   556  		expectedErrorMsg string
   557  		expectedExtMsg   []byte
   558  	}{
   559  		{
   560  			exitReason:       ExitReasonFinished,
   561  			err:              nil,
   562  			extMsg:           []byte("test finished"),
   563  			expectedState:    frameModel.WorkerStateFinished,
   564  			expectedErrorMsg: "",
   565  			expectedExtMsg:   []byte("test finished"),
   566  		},
   567  		{
   568  			exitReason:       ExitReasonFinished,
   569  			err:              errors.New("test finished with error"),
   570  			extMsg:           []byte("test finished"),
   571  			expectedState:    frameModel.WorkerStateFinished,
   572  			expectedErrorMsg: "test finished with error",
   573  			expectedExtMsg:   []byte("test finished"),
   574  		},
   575  		{
   576  			exitReason:       ExitReasonCanceled,
   577  			err:              nil,
   578  			extMsg:           []byte("test canceled"),
   579  			expectedState:    frameModel.WorkerStateStopped,
   580  			expectedErrorMsg: "",
   581  			expectedExtMsg:   []byte("test canceled"),
   582  		},
   583  		{
   584  			exitReason:       ExitReasonCanceled,
   585  			err:              errors.New("test canceled with error"),
   586  			extMsg:           []byte("test canceled"),
   587  			expectedState:    frameModel.WorkerStateStopped,
   588  			expectedErrorMsg: "test canceled with error",
   589  			expectedExtMsg:   []byte("test canceled"),
   590  		},
   591  		{
   592  			exitReason:       ExitReasonFailed,
   593  			err:              nil,
   594  			extMsg:           []byte("test failed"),
   595  			expectedState:    frameModel.WorkerStateError,
   596  			expectedErrorMsg: "",
   597  			expectedExtMsg:   []byte("test failed"),
   598  		},
   599  		{
   600  			exitReason:       ExitReasonFailed,
   601  			err:              errors.New("test failed with error"),
   602  			extMsg:           []byte("test failed"),
   603  			expectedState:    frameModel.WorkerStateError,
   604  			expectedErrorMsg: "test failed with error",
   605  			expectedExtMsg:   []byte("test failed"),
   606  		},
   607  	}
   608  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   609  	defer cancel()
   610  
   611  	for i, cs := range cases {
   612  		worker := newMockWorkerImpl(fmt.Sprintf("worker-%d", i), masterName)
   613  		worker.clock = clock.NewMock()
   614  		worker.clock.(*clock.Mock).Set(time.Now())
   615  		putMasterMeta(ctx, t, worker.metaClient, &frameModel.MasterMeta{
   616  			ID:     masterName,
   617  			NodeID: masterNodeName,
   618  			Epoch:  1,
   619  			State:  frameModel.MasterStateInit,
   620  		})
   621  
   622  		worker.On("InitImpl", mock.Anything).Return(nil)
   623  		worker.On("Status").Return(frameModel.WorkerStatus{
   624  			State: frameModel.WorkerStateNormal,
   625  		}, nil)
   626  
   627  		err := worker.Init(ctx)
   628  		require.NoError(t, err)
   629  
   630  		worker.On("Tick", mock.Anything).Return(nil)
   631  		worker.On("CloseImpl", mock.Anything).Return().Once()
   632  
   633  		err = worker.DefaultBaseWorker.Exit(ctx, cs.exitReason, cs.err, cs.extMsg)
   634  		require.NoError(t, err)
   635  
   636  		meta, err := worker.metaClient.GetWorkerByID(ctx, masterName, worker.ID())
   637  		require.NoError(t, err)
   638  		require.Equal(t, cs.expectedState, meta.State)
   639  		require.Equal(t, cs.expectedErrorMsg, meta.ErrorMsg)
   640  		require.Equal(t, cs.expectedExtMsg, meta.ExtBytes)
   641  
   642  		require.NoError(t, worker.Close(ctx))
   643  	}
   644  }