go.uber.org/cadence@v1.2.9/internal/workflow_shadower_test.go (about)

     1  // Copyright (c) 2017-2021 Uber Technologies Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package internal
    22  
    23  import (
    24  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/facebookgo/clock"
    29  	"github.com/golang/mock/gomock"
    30  	"github.com/stretchr/testify/require"
    31  	"github.com/stretchr/testify/suite"
    32  
    33  	"go.uber.org/cadence/.gen/go/cadence/workflowservicetest"
    34  	"go.uber.org/cadence/.gen/go/shared"
    35  	"go.uber.org/cadence/internal/common"
    36  )
    37  
    38  type workflowShadowerSuite struct {
    39  	*require.Assertions
    40  	suite.Suite
    41  
    42  	controller  *gomock.Controller
    43  	mockService *workflowservicetest.MockClient
    44  
    45  	testShadower        *WorkflowShadower
    46  	testWorkflowHistory *shared.History
    47  	testTimestamp       time.Time
    48  }
    49  
    50  func TestWorkflowShadowerSuite(t *testing.T) {
    51  	s := new(workflowShadowerSuite)
    52  	suite.Run(t, s)
    53  }
    54  
    55  func (s *workflowShadowerSuite) SetupTest() {
    56  	s.Assertions = require.New(s.T())
    57  
    58  	s.controller = gomock.NewController(s.T())
    59  	s.mockService = workflowservicetest.NewMockClient(s.controller)
    60  
    61  	var err error
    62  	s.testShadower, err = NewWorkflowShadower(s.mockService, "testDomain", ShadowOptions{}, ReplayOptions{}, nil)
    63  	s.NoError(err)
    64  
    65  	// overwrite shadower clock to be a mock clock
    66  	s.testShadower.clock = clock.NewMock()
    67  	// register test workflow
    68  	s.testShadower.RegisterWorkflow(testReplayWorkflow)
    69  
    70  	s.testWorkflowHistory = getTestReplayWorkflowFullHistory(s.T())
    71  
    72  	s.testTimestamp = time.Now()
    73  }
    74  
    75  func (s *workflowShadowerSuite) TearDownTest() {
    76  	s.controller.Finish()
    77  }
    78  
    79  func (s *workflowShadowerSuite) TestTimeFilterValidation() {
    80  	testCases := []struct {
    81  		msg          string
    82  		timeFilter   TimeFilter
    83  		expectErr    bool
    84  		validationFn func(TimeFilter)
    85  	}{
    86  		{
    87  			msg: "maxTimestamp before minTimestamp",
    88  			timeFilter: TimeFilter{
    89  				MinTimestamp: s.testTimestamp.Add(time.Hour),
    90  				MaxTimestamp: s.testTimestamp,
    91  			},
    92  			expectErr: true,
    93  		},
    94  		{
    95  			msg:        "neither timestamp is specified",
    96  			timeFilter: TimeFilter{},
    97  			expectErr:  false,
    98  			validationFn: func(f TimeFilter) {
    99  				s.True(f.MinTimestamp.IsZero())
   100  				s.True(f.MaxTimestamp.Equal(maxTimestamp))
   101  			},
   102  		},
   103  		{
   104  			msg: "only min timestamp is specified",
   105  			timeFilter: TimeFilter{
   106  				MinTimestamp: s.testTimestamp,
   107  			},
   108  			expectErr: false,
   109  			validationFn: func(f TimeFilter) {
   110  				s.True(f.MinTimestamp.Equal(s.testTimestamp))
   111  				s.True(f.MaxTimestamp.Equal(maxTimestamp))
   112  			},
   113  		},
   114  		{
   115  			msg: "only max timestamp is specified",
   116  			timeFilter: TimeFilter{
   117  				MaxTimestamp: s.testTimestamp,
   118  			},
   119  			expectErr: false,
   120  			validationFn: func(f TimeFilter) {
   121  				s.True(f.MinTimestamp.IsZero())
   122  				s.True(f.MaxTimestamp.Equal(s.testTimestamp))
   123  			},
   124  		},
   125  	}
   126  
   127  	for _, test := range testCases {
   128  		s.T().Run(test.msg, func(t *testing.T) {
   129  			err := test.timeFilter.validateAndPopulateFields()
   130  			if test.expectErr {
   131  				s.Error(err)
   132  				return
   133  			}
   134  
   135  			s.NoError(err)
   136  			test.validationFn(test.timeFilter)
   137  		})
   138  	}
   139  }
   140  
   141  func (s *workflowShadowerSuite) TestTimeFilterIsEmpty() {
   142  	testCases := []struct {
   143  		msg     string
   144  		filter  *TimeFilter
   145  		isEmpty bool
   146  	}{
   147  		{
   148  			msg:     "nil pointer",
   149  			filter:  nil,
   150  			isEmpty: true,
   151  		},
   152  		{
   153  			msg:     "neither field is specified",
   154  			filter:  &TimeFilter{},
   155  			isEmpty: true,
   156  		},
   157  		{
   158  			msg: "not empty",
   159  			filter: &TimeFilter{
   160  				MaxTimestamp: time.Now(),
   161  			},
   162  			isEmpty: false,
   163  		},
   164  	}
   165  
   166  	for _, test := range testCases {
   167  		s.T().Run(test.msg, func(t *testing.T) {
   168  			s.Equal(test.isEmpty, test.filter.isEmpty())
   169  		})
   170  	}
   171  }
   172  
   173  func (s *workflowShadowerSuite) TestShadowOptionsValidation() {
   174  	testCases := []struct {
   175  		msg          string
   176  		options      ShadowOptions
   177  		expectErr    bool
   178  		validationFn func(*ShadowOptions)
   179  	}{
   180  		{
   181  			msg: "exit condition not specified in continuous mode",
   182  			options: ShadowOptions{
   183  				Mode: ShadowModeContinuous,
   184  			},
   185  			expectErr: true,
   186  		},
   187  		{
   188  			msg: "both query and other filters are specified",
   189  			options: ShadowOptions{
   190  				WorkflowQuery: "some random query",
   191  				WorkflowStartTimeFilter: TimeFilter{
   192  					MinTimestamp: time.Now(),
   193  				},
   194  			},
   195  			expectErr: true,
   196  		},
   197  		{
   198  			msg:       "populate sampling rate, concurrency and status",
   199  			options:   ShadowOptions{},
   200  			expectErr: false,
   201  			validationFn: func(options *ShadowOptions) {
   202  				s.Equal("(CloseTime = missing)", options.WorkflowQuery)
   203  				s.Equal(1.0, options.SamplingRate)
   204  				s.Equal(1, options.Concurrency)
   205  			},
   206  		},
   207  		{
   208  			msg: "construct query",
   209  			options: ShadowOptions{
   210  				WorkflowTypes:  []string{"testWorkflowType"},
   211  				WorkflowStatus: []string{"open"},
   212  				WorkflowStartTimeFilter: TimeFilter{
   213  					MinTimestamp: s.testTimestamp.Add(-time.Hour),
   214  					MaxTimestamp: s.testTimestamp,
   215  				},
   216  			},
   217  			expectErr: false,
   218  			validationFn: func(options *ShadowOptions) {
   219  				expectedQuery := NewQueryBuilder().
   220  					WorkflowTypes([]string{"testWorkflowType"}).
   221  					WorkflowStatus([]WorkflowStatus{WorkflowStatusOpen}).
   222  					StartTime(
   223  						s.testTimestamp.Add(-time.Hour),
   224  						s.testTimestamp,
   225  					).Build()
   226  
   227  				s.Equal(expectedQuery, options.WorkflowQuery)
   228  			},
   229  		},
   230  	}
   231  
   232  	for _, test := range testCases {
   233  		s.T().Run(test.msg, func(t *testing.T) {
   234  			err := test.options.validateAndPopulateFields()
   235  			if test.expectErr {
   236  				s.Error(err)
   237  				return
   238  			}
   239  
   240  			s.NoError(err)
   241  			test.validationFn(&test.options)
   242  		})
   243  	}
   244  }
   245  
   246  func (s *workflowShadowerSuite) TestShadowWorkerExitCondition_ExpirationTime() {
   247  	totalWorkflows := 50
   248  	timePerWorkflow := 7 * time.Second
   249  	expirationTime := time.Minute
   250  
   251  	s.testShadower.shadowOptions.ExitCondition = ShadowExitCondition{
   252  		ExpirationInterval: expirationTime,
   253  	}
   254  
   255  	s.mockService.EXPECT().ScanWorkflowExecutions(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.ListWorkflowExecutionsResponse{
   256  		Executions:    newTestWorkflowExecutions(totalWorkflows),
   257  		NextPageToken: nil,
   258  	}, nil).Times(1)
   259  	s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).DoAndReturn(func(...interface{}) (*shared.GetWorkflowExecutionHistoryResponse, error) {
   260  		s.testShadower.clock.(*clock.Mock).Add(timePerWorkflow)
   261  		return &shared.GetWorkflowExecutionHistoryResponse{
   262  			History: s.testWorkflowHistory,
   263  		}, nil
   264  	}).Times(int(expirationTime/timePerWorkflow) + 1)
   265  
   266  	s.NoError(s.testShadower.shadowWorker())
   267  }
   268  
   269  func (s *workflowShadowerSuite) TestShadowWorkerExitCondition_MaxShadowingCount() {
   270  	maxShadowCount := 50
   271  
   272  	s.testShadower.shadowOptions.ExitCondition = ShadowExitCondition{
   273  		ShadowCount: maxShadowCount,
   274  	}
   275  
   276  	s.mockService.EXPECT().ScanWorkflowExecutions(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.ListWorkflowExecutionsResponse{
   277  		Executions:    newTestWorkflowExecutions(maxShadowCount * 2),
   278  		NextPageToken: []byte{1, 2, 3},
   279  	}, nil).Times(1)
   280  	s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.GetWorkflowExecutionHistoryResponse{
   281  		History: s.testWorkflowHistory,
   282  	}, nil).Times(maxShadowCount)
   283  
   284  	s.NoError(s.testShadower.shadowWorker())
   285  }
   286  
   287  func (s *workflowShadowerSuite) TestShadowWorker_NormalMode() {
   288  	workflowExecutions := newTestWorkflowExecutions(10)
   289  	numScan := 3
   290  	totalWorkflows := len(workflowExecutions) * numScan
   291  
   292  	for i := 0; i != numScan; i++ {
   293  		scanResp := &shared.ListWorkflowExecutionsResponse{
   294  			Executions:    workflowExecutions,
   295  			NextPageToken: []byte{1, 2, 3},
   296  		}
   297  		if i == numScan-1 {
   298  			scanResp.NextPageToken = nil
   299  		}
   300  		s.mockService.EXPECT().ScanWorkflowExecutions(gomock.Any(), gomock.Any(), callOptions()...).Return(scanResp, nil).Times(1)
   301  	}
   302  
   303  	s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.GetWorkflowExecutionHistoryResponse{
   304  		History: s.testWorkflowHistory,
   305  	}, nil).Times(totalWorkflows)
   306  
   307  	s.NoError(s.testShadower.shadowWorker())
   308  }
   309  
   310  func (s *workflowShadowerSuite) TestShadowWorker_ContinuousMode() {
   311  	workflowExecutions := newTestWorkflowExecutions(10)
   312  	numScan := 3
   313  	totalWorkflows := len(workflowExecutions) * numScan
   314  
   315  	s.testShadower.shadowOptions.Mode = ShadowModeContinuous
   316  	s.testShadower.shadowOptions.ExitCondition = ShadowExitCondition{
   317  		ShadowCount: totalWorkflows,
   318  	}
   319  
   320  	for i := 0; i != numScan; i++ {
   321  		scanResp := &shared.ListWorkflowExecutionsResponse{
   322  			Executions: workflowExecutions,
   323  		}
   324  		s.mockService.EXPECT().ScanWorkflowExecutions(gomock.Any(), gomock.Any(), callOptions()...).Return(scanResp, nil).Times(1)
   325  	}
   326  
   327  	s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.GetWorkflowExecutionHistoryResponse{
   328  		History: s.testWorkflowHistory,
   329  	}, nil).Times(totalWorkflows)
   330  
   331  	doneCh := make(chan struct{})
   332  	var advanceTimeWG sync.WaitGroup
   333  	advanceTimeWG.Add(1)
   334  	go func() {
   335  		defer advanceTimeWG.Done()
   336  		for {
   337  			time.Sleep(100 * time.Millisecond)
   338  			select {
   339  			case <-doneCh:
   340  				return
   341  			default:
   342  				s.testShadower.clock.(*clock.Mock).Add(defaultWaitDurationPerIteration)
   343  			}
   344  		}
   345  	}()
   346  
   347  	s.NoError(s.testShadower.shadowWorker())
   348  	close(doneCh)
   349  	advanceTimeWG.Wait()
   350  }
   351  
   352  func (s *workflowShadowerSuite) TestShadowWorker_ReplayFailed() {
   353  	successfullyReplayed := 5
   354  	s.mockService.EXPECT().ScanWorkflowExecutions(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.ListWorkflowExecutionsResponse{
   355  		Executions:    newTestWorkflowExecutions(successfullyReplayed * 2),
   356  		NextPageToken: []byte{1, 2, 3},
   357  	}, nil).Times(1)
   358  	s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.GetWorkflowExecutionHistoryResponse{
   359  		History: s.testWorkflowHistory,
   360  	}, nil).Times(successfullyReplayed)
   361  	s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.GetWorkflowExecutionHistoryResponse{
   362  		History: getTestReplayWorkflowMismatchHistory(s.T()),
   363  	}, nil).Times(1)
   364  
   365  	s.Error(s.testShadower.shadowWorker())
   366  }
   367  
   368  func (s *workflowShadowerSuite) TestShadowWorker_ExpectedReplayError() {
   369  	testCases := []struct {
   370  		msg                string
   371  		getHistoryErr      error
   372  		getHistoryResponse *shared.GetWorkflowExecutionHistoryResponse
   373  	}{
   374  		{
   375  			msg:           "only workflow started event", // for example cron workflow
   376  			getHistoryErr: nil,
   377  			getHistoryResponse: &shared.GetWorkflowExecutionHistoryResponse{
   378  				History: &shared.History{Events: []*shared.HistoryEvent{
   379  					createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{
   380  						WorkflowType: &shared.WorkflowType{Name: common.StringPtr("testWorkflow")},
   381  						TaskList:     &shared.TaskList{Name: common.StringPtr("taskList")},
   382  						Input:        testEncodeFunctionArgs(s.T(), getDefaultDataConverter()),
   383  						CronSchedule: common.StringPtr("* * * * *"),
   384  					}),
   385  				},
   386  				},
   387  			},
   388  		},
   389  		{
   390  			msg:                "workflow not exist",
   391  			getHistoryErr:      &shared.EntityNotExistsError{Message: "Workflow passed retention date"},
   392  			getHistoryResponse: nil,
   393  		},
   394  		{
   395  			msg:                "corrupted workflow history", // for example cron workflow
   396  			getHistoryErr:      &shared.InternalServiceError{Message: "History events not continuous"},
   397  			getHistoryResponse: nil,
   398  		},
   399  	}
   400  
   401  	for _, test := range testCases {
   402  		s.T().Run(test.msg, func(t *testing.T) {
   403  			s.mockService.EXPECT().ScanWorkflowExecutions(gomock.Any(), gomock.Any(), callOptions()...).Return(&shared.ListWorkflowExecutionsResponse{
   404  				Executions:    newTestWorkflowExecutions(1),
   405  				NextPageToken: nil,
   406  			}, nil).Times(1)
   407  			s.mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), callOptions()...).Return(test.getHistoryResponse, test.getHistoryErr).Times(1)
   408  
   409  			s.NoError(s.testShadower.shadowWorker())
   410  		})
   411  	}
   412  }
   413  
   414  func newTestWorkflowExecutions(size int) []*shared.WorkflowExecutionInfo {
   415  	executions := make([]*shared.WorkflowExecutionInfo, size)
   416  	for i := 0; i != size; i++ {
   417  		executions[i] = &shared.WorkflowExecutionInfo{
   418  			Execution: &shared.WorkflowExecution{
   419  				WorkflowId: common.StringPtr("workflowID"),
   420  				RunId:      common.StringPtr("runID"),
   421  			},
   422  		}
   423  	}
   424  	return executions
   425  }