go.uber.org/cadence@v1.2.9/internal/common/metrics/service_wrapper_test.go (about)

     1  // Copyright (c) 2017 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 metrics
    22  
    23  import (
    24  	"fmt"
    25  	"io"
    26  	"reflect"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/golang/mock/gomock"
    32  	"github.com/stretchr/testify/require"
    33  	"github.com/uber-go/tally"
    34  	"github.com/uber/tchannel-go/thrift"
    35  	"go.uber.org/yarpc"
    36  
    37  	"go.uber.org/cadence/.gen/go/cadence/workflowserviceclient"
    38  	"go.uber.org/cadence/.gen/go/cadence/workflowservicetest"
    39  	s "go.uber.org/cadence/.gen/go/shared"
    40  )
    41  
    42  var (
    43  	safeCharacters = []rune{'_'}
    44  
    45  	sanitizeOptions = tally.SanitizeOptions{
    46  		NameCharacters: tally.ValidCharacters{
    47  			Ranges:     tally.AlphanumericRange,
    48  			Characters: safeCharacters,
    49  		},
    50  		KeyCharacters: tally.ValidCharacters{
    51  			Ranges:     tally.AlphanumericRange,
    52  			Characters: safeCharacters,
    53  		},
    54  		ValueCharacters: tally.ValidCharacters{
    55  			Ranges:     tally.AlphanumericRange,
    56  			Characters: safeCharacters,
    57  		},
    58  		ReplacementCharacter: tally.DefaultReplacementCharacter,
    59  	}
    60  )
    61  
    62  type testCase struct {
    63  	serviceMethod    string
    64  	callArgs         []interface{}
    65  	mockReturns      []interface{}
    66  	expectedCounters []string
    67  }
    68  
    69  func Test_Wrapper(t *testing.T) {
    70  	ctx, _ := thrift.NewContext(time.Minute)
    71  	tests := []testCase{
    72  		// one case for each service call
    73  		{"DeprecateDomain", []interface{}{ctx, &s.DeprecateDomainRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    74  		{"DescribeDomain", []interface{}{ctx, &s.DescribeDomainRequest{}}, []interface{}{&s.DescribeDomainResponse{}, nil}, []string{CadenceRequest}},
    75  		{"GetWorkflowExecutionHistory", []interface{}{ctx, &s.GetWorkflowExecutionHistoryRequest{}}, []interface{}{&s.GetWorkflowExecutionHistoryResponse{}, nil}, []string{CadenceRequest}},
    76  		{"ListClosedWorkflowExecutions", []interface{}{ctx, &s.ListClosedWorkflowExecutionsRequest{}}, []interface{}{&s.ListClosedWorkflowExecutionsResponse{}, nil}, []string{CadenceRequest}},
    77  		{"ListOpenWorkflowExecutions", []interface{}{ctx, &s.ListOpenWorkflowExecutionsRequest{}}, []interface{}{&s.ListOpenWorkflowExecutionsResponse{}, nil}, []string{CadenceRequest}},
    78  		{"PollForActivityTask", []interface{}{ctx, &s.PollForActivityTaskRequest{}}, []interface{}{&s.PollForActivityTaskResponse{}, nil}, []string{CadenceRequest}},
    79  		{"PollForDecisionTask", []interface{}{ctx, &s.PollForDecisionTaskRequest{}}, []interface{}{&s.PollForDecisionTaskResponse{}, nil}, []string{CadenceRequest}},
    80  		{"RecordActivityTaskHeartbeat", []interface{}{ctx, &s.RecordActivityTaskHeartbeatRequest{}}, []interface{}{&s.RecordActivityTaskHeartbeatResponse{}, nil}, []string{CadenceRequest}},
    81  		{"RegisterDomain", []interface{}{ctx, &s.RegisterDomainRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    82  		{"RequestCancelWorkflowExecution", []interface{}{ctx, &s.RequestCancelWorkflowExecutionRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    83  		{"RespondActivityTaskCanceled", []interface{}{ctx, &s.RespondActivityTaskCanceledRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    84  		{"RespondActivityTaskCompleted", []interface{}{ctx, &s.RespondActivityTaskCompletedRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    85  		{"RespondActivityTaskFailed", []interface{}{ctx, &s.RespondActivityTaskFailedRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    86  		{"RespondActivityTaskCanceledByID", []interface{}{ctx, &s.RespondActivityTaskCanceledByIDRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    87  		{"RespondActivityTaskCompletedByID", []interface{}{ctx, &s.RespondActivityTaskCompletedByIDRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    88  		{"RespondActivityTaskFailedByID", []interface{}{ctx, &s.RespondActivityTaskFailedByIDRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    89  		{"RespondDecisionTaskCompleted", []interface{}{ctx, &s.RespondDecisionTaskCompletedRequest{}}, []interface{}{nil, nil}, []string{CadenceRequest}},
    90  		{"SignalWorkflowExecution", []interface{}{ctx, &s.SignalWorkflowExecutionRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    91  		{"StartWorkflowExecution", []interface{}{ctx, &s.StartWorkflowExecutionRequest{}}, []interface{}{&s.StartWorkflowExecutionResponse{}, nil}, []string{CadenceRequest}},
    92  		{"TerminateWorkflowExecution", []interface{}{ctx, &s.TerminateWorkflowExecutionRequest{}}, []interface{}{nil}, []string{CadenceRequest}},
    93  		{"ResetWorkflowExecution", []interface{}{ctx, &s.ResetWorkflowExecutionRequest{}}, []interface{}{&s.ResetWorkflowExecutionResponse{}, nil}, []string{CadenceRequest}},
    94  		{"UpdateDomain", []interface{}{ctx, &s.UpdateDomainRequest{}}, []interface{}{&s.UpdateDomainResponse{}, nil}, []string{CadenceRequest}},
    95  		// one case of invalid request
    96  		{"PollForActivityTask", []interface{}{ctx, &s.PollForActivityTaskRequest{}}, []interface{}{nil, &s.EntityNotExistsError{}}, []string{CadenceRequest, CadenceInvalidRequest}},
    97  		// one case of server error
    98  		{"PollForActivityTask", []interface{}{ctx, &s.PollForActivityTaskRequest{}}, []interface{}{nil, &s.InternalServiceError{}}, []string{CadenceRequest, CadenceError}},
    99  		{"QueryWorkflow", []interface{}{ctx, &s.QueryWorkflowRequest{}}, []interface{}{nil, &s.InternalServiceError{}}, []string{CadenceRequest, CadenceError}},
   100  		{"RespondQueryTaskCompleted", []interface{}{ctx, &s.RespondQueryTaskCompletedRequest{}}, []interface{}{&s.InternalServiceError{}}, []string{CadenceRequest, CadenceError}},
   101  	}
   102  
   103  	// run each test twice - once with the regular scope, once with a sanitized metrics scope
   104  	for _, test := range tests {
   105  		runTest(t, test, newService, assertMetrics, fmt.Sprintf("%v_normal", test.serviceMethod))
   106  		runTest(t, test, newPromService, assertPromMetrics, fmt.Sprintf("%v_prom_sanitized", test.serviceMethod))
   107  	}
   108  }
   109  
   110  func runTest(
   111  	t *testing.T,
   112  	test testCase,
   113  	serviceFunc func(*testing.T) (*workflowservicetest.MockClient, workflowserviceclient.Interface, io.Closer, *CapturingStatsReporter),
   114  	validationFunc func(*testing.T, *CapturingStatsReporter, string, []string),
   115  	name string,
   116  ) {
   117  	t.Run(name, func(t *testing.T) {
   118  		t.Parallel()
   119  		// gomock mutates the returns slice, which leads to different test values between the two runs.
   120  		// copy the slice until gomock fixes it: https://github.com/golang/mock/issues/353
   121  		returns := append(make([]interface{}, 0, len(test.mockReturns)), test.mockReturns...)
   122  
   123  		mockService, wrapperService, closer, reporter := serviceFunc(t)
   124  		switch test.serviceMethod {
   125  		case "DeprecateDomain":
   126  			mockService.EXPECT().DeprecateDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   127  		case "DescribeDomain":
   128  			mockService.EXPECT().DescribeDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   129  		case "GetWorkflowExecutionHistory":
   130  			mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   131  		case "ListClosedWorkflowExecutions":
   132  			mockService.EXPECT().ListClosedWorkflowExecutions(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   133  		case "ListOpenWorkflowExecutions":
   134  			mockService.EXPECT().ListOpenWorkflowExecutions(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   135  		case "PollForActivityTask":
   136  			mockService.EXPECT().PollForActivityTask(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   137  		case "PollForDecisionTask":
   138  			mockService.EXPECT().PollForDecisionTask(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   139  		case "RecordActivityTaskHeartbeat":
   140  			mockService.EXPECT().RecordActivityTaskHeartbeat(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   141  		case "RecordActivityTaskHeartbeatByID":
   142  			mockService.EXPECT().RecordActivityTaskHeartbeatByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   143  		case "RegisterDomain":
   144  			mockService.EXPECT().RegisterDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   145  		case "RequestCancelWorkflowExecution":
   146  			mockService.EXPECT().RequestCancelWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   147  		case "RespondActivityTaskCanceled":
   148  			mockService.EXPECT().RespondActivityTaskCanceled(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   149  		case "RespondActivityTaskCompleted":
   150  			mockService.EXPECT().RespondActivityTaskCompleted(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   151  		case "RespondActivityTaskFailed":
   152  			mockService.EXPECT().RespondActivityTaskFailed(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   153  		case "RespondActivityTaskCanceledByID":
   154  			mockService.EXPECT().RespondActivityTaskCanceledByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   155  		case "RespondActivityTaskCompletedByID":
   156  			mockService.EXPECT().RespondActivityTaskCompletedByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   157  		case "RespondActivityTaskFailedByID":
   158  			mockService.EXPECT().RespondActivityTaskFailedByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   159  		case "RespondDecisionTaskCompleted":
   160  			mockService.EXPECT().RespondDecisionTaskCompleted(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   161  		case "SignalWorkflowExecution":
   162  			mockService.EXPECT().SignalWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   163  		case "SignaWithStartlWorkflowExecution":
   164  			mockService.EXPECT().SignalWithStartWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   165  		case "StartWorkflowExecution":
   166  			mockService.EXPECT().StartWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   167  		case "TerminateWorkflowExecution":
   168  			mockService.EXPECT().TerminateWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   169  		case "ResetWorkflowExecution":
   170  			mockService.EXPECT().ResetWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   171  		case "UpdateDomain":
   172  			mockService.EXPECT().UpdateDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   173  		case "QueryWorkflow":
   174  			mockService.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   175  		case "RespondQueryTaskCompleted":
   176  			mockService.EXPECT().RespondQueryTaskCompleted(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...)
   177  		}
   178  
   179  		callOption := yarpc.CallOption{}
   180  		inputs := make([]reflect.Value, len(test.callArgs))
   181  		for i, arg := range test.callArgs {
   182  			inputs[i] = reflect.ValueOf(arg)
   183  		}
   184  		inputs = append(inputs, reflect.ValueOf(callOption))
   185  		method := reflect.ValueOf(wrapperService).MethodByName(test.serviceMethod)
   186  		method.Call(inputs)
   187  		require.NoError(t, closer.Close())
   188  		validationFunc(t, reporter, test.serviceMethod, test.expectedCounters)
   189  	})
   190  }
   191  
   192  func assertMetrics(t *testing.T, reporter *CapturingStatsReporter, methodName string, counterNames []string) {
   193  	require.Equal(t, len(counterNames), len(reporter.counts))
   194  	for _, name := range counterNames {
   195  		counterName := CadenceMetricsPrefix + methodName + "." + name
   196  		find := false
   197  		// counters are not in order
   198  		for _, counter := range reporter.counts {
   199  			if counterName == counter.name {
   200  				find = true
   201  				break
   202  			}
   203  		}
   204  		require.True(t, find)
   205  	}
   206  	require.Equal(t, 1, len(reporter.timers))
   207  	require.Equal(t, CadenceMetricsPrefix+methodName+"."+CadenceLatency, reporter.timers[0].name)
   208  }
   209  
   210  func assertPromMetrics(t *testing.T, reporter *CapturingStatsReporter, methodName string, counterNames []string) {
   211  	require.Equal(t, len(counterNames), len(reporter.counts))
   212  	for _, name := range counterNames {
   213  		counterName := makePromCompatible(CadenceMetricsPrefix + methodName + "." + name)
   214  		find := false
   215  		// counters are not in order
   216  		for _, counter := range reporter.counts {
   217  			if counterName == counter.name {
   218  				find = true
   219  				break
   220  			}
   221  		}
   222  		require.True(t, find)
   223  	}
   224  	require.Equal(t, 1, len(reporter.timers))
   225  	expected := makePromCompatible(CadenceMetricsPrefix + methodName + "." + CadenceLatency)
   226  	require.Equal(t, expected, reporter.timers[0].name)
   227  }
   228  
   229  func makePromCompatible(name string) string {
   230  	name = strings.Replace(name, "-", "_", -1)
   231  	name = strings.Replace(name, ".", "_", -1)
   232  	return name
   233  }
   234  
   235  func newService(t *testing.T) (
   236  	mockService *workflowservicetest.MockClient,
   237  	wrapperService workflowserviceclient.Interface,
   238  	closer io.Closer,
   239  	reporter *CapturingStatsReporter,
   240  ) {
   241  	mockCtrl := gomock.NewController(t)
   242  	mockService = workflowservicetest.NewMockClient(mockCtrl)
   243  	isReplay := false
   244  	scope, closer, reporter := NewMetricsScope(&isReplay)
   245  	wrapperService = NewWorkflowServiceWrapper(mockService, scope)
   246  	return
   247  }
   248  
   249  func newPromService(t *testing.T) (
   250  	mockService *workflowservicetest.MockClient,
   251  	wrapperService workflowserviceclient.Interface,
   252  	closer io.Closer,
   253  	reporter *CapturingStatsReporter,
   254  ) {
   255  	mockCtrl := gomock.NewController(t)
   256  	mockService = workflowservicetest.NewMockClient(mockCtrl)
   257  	isReplay := false
   258  	scope, closer, reporter := newPromScope(&isReplay)
   259  	wrapperService = NewWorkflowServiceWrapper(mockService, scope)
   260  	return
   261  }
   262  
   263  func newPromScope(isReplay *bool) (tally.Scope, io.Closer, *CapturingStatsReporter) {
   264  	reporter := &CapturingStatsReporter{}
   265  	opts := tally.ScopeOptions{
   266  		Reporter:        reporter,
   267  		SanitizeOptions: &sanitizeOptions,
   268  	}
   269  	scope, closer := tally.NewRootScope(opts, time.Second)
   270  	return WrapScope(isReplay, scope, &realClock{}), closer, reporter
   271  }