go.temporal.io/server@v1.23.0/common/searchattribute/manager_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package searchattribute
    26  
    27  import (
    28  	"context"
    29  	"errors"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/golang/mock/gomock"
    35  	"github.com/stretchr/testify/require"
    36  	"github.com/stretchr/testify/suite"
    37  	enumspb "go.temporal.io/api/enums/v1"
    38  	"go.temporal.io/api/serviceerror"
    39  
    40  	persistencespb "go.temporal.io/server/api/persistence/v1"
    41  	"go.temporal.io/server/common/clock"
    42  	"go.temporal.io/server/common/log"
    43  	"go.temporal.io/server/common/persistence"
    44  )
    45  
    46  type (
    47  	searchAttributesManagerSuite struct {
    48  		suite.Suite
    49  		*require.Assertions
    50  
    51  		controller *gomock.Controller
    52  
    53  		logger                     log.Logger
    54  		timeSource                 *clock.EventTimeSource
    55  		mockClusterMetadataManager *persistence.MockClusterMetadataManager
    56  		manager                    *managerImpl
    57  		forceCacheRefresh          bool
    58  	}
    59  )
    60  
    61  func TestSearchAttributesManagerSuite(t *testing.T) {
    62  	suite.Run(t, &searchAttributesManagerSuite{})
    63  }
    64  
    65  func (s *searchAttributesManagerSuite) SetupSuite() {
    66  }
    67  
    68  func (s *searchAttributesManagerSuite) TearDownSuite() {
    69  
    70  }
    71  
    72  func (s *searchAttributesManagerSuite) SetupTest() {
    73  	s.Assertions = require.New(s.T())
    74  	s.controller = gomock.NewController(s.T())
    75  	s.logger = log.NewTestLogger()
    76  	s.timeSource = clock.NewEventTimeSource()
    77  	s.mockClusterMetadataManager = persistence.NewMockClusterMetadataManager(s.controller)
    78  	s.manager = NewManager(
    79  		s.timeSource,
    80  		s.mockClusterMetadataManager,
    81  		func() bool {
    82  			return s.forceCacheRefresh
    83  		},
    84  	)
    85  }
    86  
    87  func (s *searchAttributesManagerSuite) TearDownTest() {
    88  	s.controller.Finish()
    89  }
    90  
    91  func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache() {
    92  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 0, 0, time.UTC))
    93  	// Initial call
    94  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
    95  		ClusterMetadata: &persistencespb.ClusterMetadata{
    96  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
    97  				"index-name": {
    98  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
    99  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   100  					}}},
   101  		},
   102  		Version: 1,
   103  	}, nil)
   104  	// Second call, no changes in DB (version is the same)
   105  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 10, 0, time.UTC))
   106  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   107  		ClusterMetadata: &persistencespb.ClusterMetadata{
   108  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   109  				"index-name": {
   110  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   111  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   112  					}}},
   113  		},
   114  		Version: 1,
   115  	}, nil)
   116  	// Third call, DB changed
   117  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 20, 0, time.UTC))
   118  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   119  		ClusterMetadata: &persistencespb.ClusterMetadata{
   120  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   121  				"index-name": {
   122  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   123  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   124  					}}},
   125  		},
   126  		Version: 2,
   127  	}, nil)
   128  
   129  	wg := sync.WaitGroup{}
   130  	wg.Add(10)
   131  	for goroutine := 0; goroutine < 10; goroutine++ {
   132  		go func(goroutine int) {
   133  			defer wg.Done()
   134  			for i := 1; i < 1500; i++ {
   135  				searchAttributes, err := s.manager.GetSearchAttributes("index-name", false)
   136  				s.NoError(err)
   137  				s.Len(searchAttributes.Custom(), 1)
   138  				t, err := searchAttributes.GetType("OrderId")
   139  				s.NoError(err)
   140  				s.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, t)
   141  				if i%500 == 0 && goroutine == 5 {
   142  					// This moves time two times.
   143  					s.timeSource.Update(s.timeSource.Now().Add(cacheRefreshInterval).Add(time.Second))
   144  				}
   145  			}
   146  		}(goroutine)
   147  	}
   148  	wg.Wait()
   149  }
   150  
   151  func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache_Error() {
   152  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 0, 0, time.UTC))
   153  	// Initial call
   154  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(nil, errors.New("random error"))
   155  	searchAttributes, err := s.manager.GetSearchAttributes("index-name", false)
   156  	s.Error(err)
   157  	s.Len(searchAttributes.Custom(), 0)
   158  }
   159  
   160  func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache_NotFoundError() {
   161  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 0, 0, time.UTC))
   162  
   163  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(nil, serviceerror.NewNotFound("not found"))
   164  	searchAttributes, err := s.manager.GetSearchAttributes("index-name", false)
   165  	s.NoError(err)
   166  	s.Len(searchAttributes.Custom(), 0)
   167  
   168  	// GetClusterMetadata() shouldn't be called, because results are cached.
   169  	searchAttributes, err = s.manager.GetSearchAttributes("index-name", false)
   170  	s.NoError(err)
   171  	s.Len(searchAttributes.Custom(), 0)
   172  }
   173  
   174  func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache_UnavailableError() {
   175  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 0, 0, time.UTC))
   176  
   177  	// First call populates cache.
   178  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   179  		ClusterMetadata: &persistencespb.ClusterMetadata{
   180  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   181  				"index-name": {
   182  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   183  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   184  					}}},
   185  		},
   186  		Version: 1,
   187  	}, nil)
   188  	searchAttributes, err := s.manager.GetSearchAttributes("index-name", false)
   189  	s.NoError(err)
   190  	s.Len(searchAttributes.Custom(), 1)
   191  
   192  	// Expire cache.
   193  	s.timeSource.Update(time.Date(2020, 8, 22, 2, 0, 0, 0, time.UTC))
   194  
   195  	// Second call, cache is expired, DB is down, but cache data is returned.
   196  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(nil, serviceerror.NewUnavailable("db is down"))
   197  	searchAttributes, err = s.manager.GetSearchAttributes("index-name", false)
   198  	s.NoError(err)
   199  	s.Len(searchAttributes.Custom(), 1)
   200  
   201  	// Next cache refresh in cacheRefreshIfUnavailableInterval.
   202  	c := s.manager.cache.Load().(cache)
   203  	s.Equal(time.Date(2020, 8, 22, 2, 0, 20, 0, time.UTC), c.expireOn)
   204  }
   205  
   206  func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache_EmptyIndex() {
   207  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   208  		ClusterMetadata: &persistencespb.ClusterMetadata{
   209  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   210  				"": {
   211  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   212  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   213  					}}},
   214  		},
   215  		Version: 1,
   216  	}, nil)
   217  
   218  	searchAttributes, err := s.manager.GetSearchAttributes("", false)
   219  	s.NoError(err)
   220  	s.Len(searchAttributes.Custom(), 1)
   221  }
   222  
   223  func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache_RefreshIfAbsent() {
   224  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 0, 0, time.UTC))
   225  
   226  	// First call populates cache.
   227  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   228  		ClusterMetadata: &persistencespb.ClusterMetadata{
   229  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{},
   230  		},
   231  		Version: 1,
   232  	}, nil)
   233  
   234  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   235  		ClusterMetadata: &persistencespb.ClusterMetadata{
   236  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   237  				"index-name": {
   238  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   239  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   240  					}}},
   241  		},
   242  		Version: 2,
   243  	}, nil)
   244  
   245  	searchAttributes, err := s.manager.GetSearchAttributes("index-name", false)
   246  	s.NoError(err)
   247  	s.Len(searchAttributes.Custom(), 0)
   248  
   249  	s.timeSource.Update(time.Date(2020, 8, 22, 1, 0, 1, 0, time.UTC))
   250  
   251  	searchAttributes, err = s.manager.GetSearchAttributes("index-name", false)
   252  	s.NoError(err)
   253  	s.Len(searchAttributes.Custom(), 0)
   254  
   255  	s.forceCacheRefresh = true
   256  	searchAttributes, err = s.manager.GetSearchAttributes("index-name", false)
   257  	s.NoError(err)
   258  	s.Len(searchAttributes.Custom(), 1)
   259  }
   260  
   261  func (s *searchAttributesManagerSuite) TestSaveSearchAttributes_UpdateIndex() {
   262  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   263  		ClusterMetadata: &persistencespb.ClusterMetadata{
   264  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   265  				"index-name": {
   266  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   267  						"OrderIdOld": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   268  					}}},
   269  		},
   270  		Version: 1,
   271  	}, nil)
   272  
   273  	s.mockClusterMetadataManager.EXPECT().SaveClusterMetadata(gomock.Any(), &persistence.SaveClusterMetadataRequest{
   274  		ClusterMetadata: &persistencespb.ClusterMetadata{
   275  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   276  				"index-name": {
   277  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   278  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   279  					}}},
   280  		},
   281  		Version: 1,
   282  	}).Return(false, nil)
   283  
   284  	err := s.manager.SaveSearchAttributes(context.Background(), "index-name", map[string]enumspb.IndexedValueType{
   285  		"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   286  	})
   287  	s.NoError(err)
   288  }
   289  func (s *searchAttributesManagerSuite) TestSaveSearchAttributes_NewIndex() {
   290  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   291  		ClusterMetadata: &persistencespb.ClusterMetadata{
   292  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   293  				"index-name-2": {
   294  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   295  						"OrderId2": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   296  					}}},
   297  		},
   298  		Version: 1,
   299  	}, nil)
   300  
   301  	s.mockClusterMetadataManager.EXPECT().SaveClusterMetadata(gomock.Any(), &persistence.SaveClusterMetadataRequest{
   302  		ClusterMetadata: &persistencespb.ClusterMetadata{
   303  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   304  				"index-name-2": {
   305  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   306  						"OrderId2": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   307  					}},
   308  				"index-name": {
   309  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   310  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   311  					}}},
   312  		},
   313  		Version: 1,
   314  	}).Return(false, nil)
   315  
   316  	err := s.manager.SaveSearchAttributes(context.Background(), "index-name", map[string]enumspb.IndexedValueType{
   317  		"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   318  	})
   319  	s.NoError(err)
   320  }
   321  
   322  func (s *searchAttributesManagerSuite) TestSaveSearchAttributesCache_EmptyIndex() {
   323  	s.mockClusterMetadataManager.EXPECT().GetCurrentClusterMetadata(gomock.Any()).Return(&persistence.GetClusterMetadataResponse{
   324  		ClusterMetadata: &persistencespb.ClusterMetadata{
   325  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   326  				"index-name-2": {
   327  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   328  						"OrderId2": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   329  					}}},
   330  		},
   331  		Version: 1,
   332  	}, nil)
   333  
   334  	s.mockClusterMetadataManager.EXPECT().SaveClusterMetadata(gomock.Any(), &persistence.SaveClusterMetadataRequest{
   335  		ClusterMetadata: &persistencespb.ClusterMetadata{
   336  			IndexSearchAttributes: map[string]*persistencespb.IndexSearchAttributes{
   337  				"index-name-2": {
   338  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   339  						"OrderId2": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   340  					}},
   341  				"": {
   342  					CustomSearchAttributes: map[string]enumspb.IndexedValueType{
   343  						"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   344  					}}},
   345  		},
   346  		Version: 1,
   347  	}).Return(false, nil)
   348  
   349  	err := s.manager.SaveSearchAttributes(context.Background(), "", map[string]enumspb.IndexedValueType{
   350  		"OrderId": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   351  	})
   352  	s.NoError(err)
   353  }