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 }