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  }