go.uber.org/cadence@v1.2.9/evictiontest/workflow_cache_eviction_test.go (about) 1 // Copyright (c) 2017-2020 Uber Technologies Inc. 2 // Portions of the Software are attributed to Copyright (c) 2020 Temporal Technologies Inc. 3 // 4 // Permission is hereby granted, free of charge, to any person obtaining a copy 5 // of this software and associated documentation files (the "Software"), to deal 6 // in the Software without restriction, including without limitation the rights 7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 // copies of the Software, and to permit persons to whom the Software is 9 // furnished to do so, subject to the following conditions: 10 // 11 // The above copyright notice and this permission notice shall be included in 12 // all copies or substantial portions of the Software. 13 // 14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 // THE SOFTWARE. 21 22 // This test must be its own package because workflow execution cache 23 // is package-level global variable, so any tests against it should belong to 24 // its own package to avoid inter-test interference because "go test" command 25 // builds one test binary per go package(even if the tests in the package are split 26 // among multiple .go source files) and then uses reflection on the per package 27 // binary to run tests. 28 // This means any test whose result hinges on having its own exclusive own of globals 29 // should be put in its own package to avoid conflicts in global variable accesses. 30 package evictiontest 31 32 import ( 33 "strconv" 34 "testing" 35 "time" 36 37 "github.com/golang/mock/gomock" 38 "github.com/stretchr/testify/suite" 39 "go.uber.org/atomic" 40 "go.uber.org/yarpc" 41 "go.uber.org/zap/zaptest" 42 "golang.org/x/net/context" 43 44 "go.uber.org/cadence/.gen/go/cadence/workflowservicetest" 45 m "go.uber.org/cadence/.gen/go/shared" 46 "go.uber.org/cadence/internal" 47 "go.uber.org/cadence/internal/common" 48 "go.uber.org/cadence/worker" 49 ) 50 51 // copied from internal/test_helpers_test.go 52 // this is the mock for yarpcCallOptions, as gomock requires the num of arguments to be the same. 53 // see getYarpcCallOptions for the default case. 54 func callOptions() []interface{} { 55 return []interface{}{ 56 gomock.Any(), // library version 57 gomock.Any(), // feature version 58 gomock.Any(), // client name 59 gomock.Any(), // feature flags 60 gomock.Any(), // isolation group 61 } 62 } 63 64 func testReplayWorkflow(ctx internal.Context) error { 65 ao := internal.ActivityOptions{ 66 ScheduleToStartTimeout: time.Second, 67 StartToCloseTimeout: time.Second, 68 } 69 ctx = internal.WithActivityOptions(ctx, ao) 70 err := internal.ExecuteActivity(ctx, "testActivity").Get(ctx, nil) 71 if err != nil { 72 panic("Failed workflow") 73 } 74 return err 75 } 76 77 type ( 78 CacheEvictionSuite struct { 79 suite.Suite 80 mockCtrl *gomock.Controller 81 service *workflowservicetest.MockClient 82 } 83 ) 84 85 // Test suite. 86 func (s *CacheEvictionSuite) SetupTest() { 87 s.mockCtrl = gomock.NewController(s.T()) 88 s.service = workflowservicetest.NewMockClient(s.mockCtrl) 89 } 90 91 func (s *CacheEvictionSuite) TearDownTest() { 92 s.mockCtrl.Finish() // assert mock’s expectations 93 } 94 95 func TestWorkersTestSuite(t *testing.T) { 96 suite.Run(t, new(CacheEvictionSuite)) 97 } 98 99 func createTestEventWorkflowExecutionStarted(eventID int64, attr *m.WorkflowExecutionStartedEventAttributes) *m.HistoryEvent { 100 return &m.HistoryEvent{ 101 EventId: common.Int64Ptr(eventID), 102 EventType: common.EventTypePtr(m.EventTypeWorkflowExecutionStarted), 103 WorkflowExecutionStartedEventAttributes: attr, 104 } 105 } 106 107 func createTestEventDecisionTaskScheduled(eventID int64, attr *m.DecisionTaskScheduledEventAttributes) *m.HistoryEvent { 108 return &m.HistoryEvent{ 109 EventId: common.Int64Ptr(eventID), 110 EventType: common.EventTypePtr(m.EventTypeDecisionTaskScheduled), 111 DecisionTaskScheduledEventAttributes: attr, 112 } 113 } 114 115 func (s *CacheEvictionSuite) TestResetStickyOnEviction() { 116 testEvents := []*m.HistoryEvent{ 117 createTestEventWorkflowExecutionStarted(1, &m.WorkflowExecutionStartedEventAttributes{ 118 TaskList: &m.TaskList{Name: common.StringPtr("tasklist")}, 119 }), 120 createTestEventDecisionTaskScheduled(2, &m.DecisionTaskScheduledEventAttributes{}), 121 } 122 123 var taskCounter atomic.Int32 // lambda variable to keep count 124 // mock that manufactures unique decision tasks 125 mockPollForDecisionTask := func( 126 ctx context.Context, 127 _PollRequest *m.PollForDecisionTaskRequest, 128 opts ...yarpc.CallOption, 129 ) (success *m.PollForDecisionTaskResponse, err error) { 130 taskID := taskCounter.Inc() 131 workflowID := common.StringPtr("testID" + strconv.Itoa(int(taskID))) 132 runID := common.StringPtr("runID" + strconv.Itoa(int(taskID))) 133 // how we initialize the response here is the result of a series of trial and error 134 // the goal is we want to fabricate a response that looks real enough to our worker 135 // that it will actually go along with processing it instead of just tossing it out 136 // after polling it or giving an error 137 ret := &m.PollForDecisionTaskResponse{ 138 TaskToken: make([]byte, 5), 139 WorkflowExecution: &m.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, 140 WorkflowType: &m.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/evictiontest.testReplayWorkflow")}, 141 History: &m.History{Events: testEvents}, 142 PreviousStartedEventId: common.Int64Ptr(5)} 143 return ret, nil 144 } 145 146 resetStickyAPICalled := make(chan struct{}) 147 mockResetStickyTaskList := func( 148 ctx context.Context, 149 _ResetRequest *m.ResetStickyTaskListRequest, 150 opts ...yarpc.CallOption, 151 ) (success *m.ResetStickyTaskListResponse, err error) { 152 resetStickyAPICalled <- struct{}{} 153 return &m.ResetStickyTaskListResponse{}, nil 154 } 155 // pick 5 as cache size because it's not too big and not too small. 156 cacheSize := 5 157 internal.SetStickyWorkflowCacheSize(cacheSize) 158 // once for workflow worker because we disable activity worker 159 s.service.EXPECT().DescribeDomain(gomock.Any(), gomock.Any(), callOptions()...).Return(nil, nil).Times(1) 160 // feed our worker exactly *cacheSize* "legit" decision tasks 161 // these are handcrafted decision tasks that are not blatantly obviously mocks 162 // the goal is to trick our worker into thinking they are real so it 163 // actually goes along with processing these and puts their execution in the cache. 164 s.service.EXPECT().PollForDecisionTask(gomock.Any(), gomock.Any(), callOptions()...).DoAndReturn(mockPollForDecisionTask).Times(cacheSize) 165 // after *cacheSize* "legit" tasks are fed to our worker, start feeding our worker empty responses. 166 // these will get tossed away immediately after polled, but we still need them so gomock doesn't compain about unexpected calls. 167 // this is because our worker's poller doesn't stop, it keeps polling on the service client as long 168 // as Stop() is not called on the worker 169 s.service.EXPECT().PollForDecisionTask(gomock.Any(), gomock.Any(), callOptions()...).Return(&m.PollForDecisionTaskResponse{}, nil).AnyTimes() 170 // this gets called after polled decision tasks are processed, any number of times doesn't matter 171 s.service.EXPECT().RespondDecisionTaskCompleted(gomock.Any(), gomock.Any(), callOptions()...).Return(&m.RespondDecisionTaskCompletedResponse{}, nil).AnyTimes() 172 // this is the critical point of the test. 173 // ResetSticky should be called exactly once because our workflow cache evicts when full 174 // so if our worker puts *cacheSize* entries in the cache, it should evict exactly one 175 s.service.EXPECT().ResetStickyTaskList(gomock.Any(), gomock.Any(), callOptions()...).DoAndReturn(mockResetStickyTaskList).Times(1) 176 177 workflowWorker := internal.NewWorker(s.service, "test-domain", "tasklist", worker.Options{ 178 DisableActivityWorker: true, 179 Logger: zaptest.NewLogger(s.T()), 180 IsolationGroup: "zone-1", 181 }) 182 // this is an arbitrary workflow we use for this test 183 // NOTE: a simple helloworld that doesn't execute an activity 184 // won't work because the workflow will simply just complete 185 // and won't stay in the cache. 186 // for this test, we need a workflow that "blocks" either by 187 // running an activity or waiting on a timer so that its execution 188 // context sticks around in the cache. 189 workflowWorker.RegisterWorkflow(testReplayWorkflow) 190 191 workflowWorker.Start() 192 193 testTimedOut := false 194 select { 195 case <-time.After(time.Second * 5): 196 testTimedOut = true 197 case <-resetStickyAPICalled: 198 // success 199 } 200 201 workflowWorker.Stop() 202 s.Equal(testTimedOut, false) 203 }