go.temporal.io/server@v1.23.0/common/persistence/visibility/store/elasticsearch/visibility_store_read_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 elasticsearch 26 27 import ( 28 "context" 29 "encoding/json" 30 "errors" 31 "fmt" 32 "math" 33 "strings" 34 "testing" 35 "time" 36 37 "github.com/golang/mock/gomock" 38 "github.com/olivere/elastic/v7" 39 "github.com/stretchr/testify/require" 40 "github.com/stretchr/testify/suite" 41 commonpb "go.temporal.io/api/common/v1" 42 enumspb "go.temporal.io/api/enums/v1" 43 "go.temporal.io/api/serviceerror" 44 "go.temporal.io/api/workflowservice/v1" 45 "google.golang.org/protobuf/types/known/timestamppb" 46 47 "go.temporal.io/api/temporalproto" 48 "go.temporal.io/server/common/debug" 49 "go.temporal.io/server/common/dynamicconfig" 50 "go.temporal.io/server/common/metrics" 51 "go.temporal.io/server/common/namespace" 52 "go.temporal.io/server/common/persistence/visibility/manager" 53 "go.temporal.io/server/common/persistence/visibility/store/elasticsearch/client" 54 "go.temporal.io/server/common/persistence/visibility/store/query" 55 "go.temporal.io/server/common/searchattribute" 56 "go.temporal.io/server/common/testing/protorequire" 57 ) 58 59 type ( 60 ESVisibilitySuite struct { 61 suite.Suite 62 // override suite.Suite.Assertions with require.Assertions; this means that s.NotNil(nil) will stop the test, not merely log an error 63 *require.Assertions 64 protorequire.ProtoAssertions 65 controller *gomock.Controller 66 visibilityStore *visibilityStore 67 mockESClient *client.MockClient 68 mockProcessor *MockProcessor 69 mockMetricsHandler *metrics.MockHandler 70 mockSearchAttributesMapperProvider *searchattribute.MockMapperProvider 71 } 72 ) 73 74 var ( 75 testIndex = "test-index" 76 testNamespace = namespace.Name("test-namespace") 77 testNamespaceID = namespace.ID("bfd5c907-f899-4baf-a7b2-2ab85e623ebd") 78 testNSDivision = "hidden-stuff" 79 testPageSize = 5 80 testEarliestTime = time.Unix(0, 1547596872371000000).UTC() 81 testLatestTime = time.Unix(0, 2547596872371000000).UTC() 82 testWorkflowType = "test-wf-type" 83 testWorkflowID = "test-wid" 84 testRunID = "test-rid" 85 testStatus = enumspb.WORKFLOW_EXECUTION_STATUS_FAILED 86 87 testSearchResult = &elastic.SearchResult{ 88 Hits: &elastic.SearchHits{}, 89 } 90 errTestESSearch = errors.New("ES error") 91 92 filterOpen = fmt.Sprintf("map[term:map[ExecutionStatus:%s]", enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING) 93 filterCloseRE = fmt.Sprintf(`must_not:\[?map\[term:map\[ExecutionStatus:%s\]\]`, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING) 94 filterByType = fmt.Sprintf("map[term:map[WorkflowType:%s]", testWorkflowType) 95 filterByWID = fmt.Sprintf("map[term:map[WorkflowId:%s]", testWorkflowID) 96 filterByExecutionStatus = fmt.Sprintf("map[term:map[ExecutionStatus:%s]", testStatus.String()) 97 filterByNSDivision = fmt.Sprintf("map[term:map[TemporalNamespaceDivision:%s]", testNSDivision) 98 99 namespaceDivisionExists = elastic.NewExistsQuery(searchattribute.TemporalNamespaceDivision) 100 ) 101 102 func createTestRequest() *manager.ListWorkflowExecutionsRequest { 103 return &manager.ListWorkflowExecutionsRequest{ 104 NamespaceID: testNamespaceID, 105 Namespace: testNamespace, 106 PageSize: testPageSize, 107 EarliestStartTime: testEarliestTime, 108 LatestStartTime: testLatestTime, 109 } 110 } 111 112 func createTestRequestWithNSDivision() *manager.ListWorkflowExecutionsRequest { 113 req := createTestRequest() 114 req.NamespaceDivision = testNSDivision 115 return req 116 } 117 118 func TestESVisibilitySuite(t *testing.T) { 119 suite.Run(t, new(ESVisibilitySuite)) 120 } 121 122 func (s *ESVisibilitySuite) SetupTest() { 123 // Have to define our overridden assertions in the test setup. If we did it earlier, s.T() will return nil 124 s.Assertions = require.New(s.T()) 125 s.ProtoAssertions = protorequire.New(s.T()) 126 127 esProcessorAckTimeout := dynamicconfig.GetDurationPropertyFn(1 * time.Minute * debug.TimeoutMultiplier) 128 visibilityDisableOrderByClause := dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false) 129 visibilityEnableManualPagination := dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true) 130 131 s.controller = gomock.NewController(s.T()) 132 s.mockMetricsHandler = metrics.NewMockHandler(s.controller) 133 s.mockMetricsHandler.EXPECT().WithTags(metrics.OperationTag(metrics.ElasticsearchVisibility)).Return(s.mockMetricsHandler).AnyTimes() 134 s.mockProcessor = NewMockProcessor(s.controller) 135 s.mockESClient = client.NewMockClient(s.controller) 136 s.mockSearchAttributesMapperProvider = searchattribute.NewMockMapperProvider(s.controller) 137 s.visibilityStore = NewVisibilityStore( 138 s.mockESClient, 139 testIndex, 140 searchattribute.NewTestProvider(), 141 searchattribute.NewTestMapperProvider(nil), 142 s.mockProcessor, 143 esProcessorAckTimeout, 144 visibilityDisableOrderByClause, 145 visibilityEnableManualPagination, 146 s.mockMetricsHandler, 147 ) 148 } 149 150 func (s *ESVisibilitySuite) TearDownTest() { 151 s.controller.Finish() 152 } 153 154 func (s *ESVisibilitySuite) TestListOpenWorkflowExecutions() { 155 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 156 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 157 source, _ := input.Query.Source() 158 s.Contains(fmt.Sprintf("%v", source), filterOpen) 159 return testSearchResult, nil 160 }) 161 _, err := s.visibilityStore.ListOpenWorkflowExecutions(context.Background(), createTestRequest()) 162 s.NoError(err) 163 164 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 165 _, err = s.visibilityStore.ListOpenWorkflowExecutions(context.Background(), createTestRequest()) 166 s.Error(err) 167 _, ok := err.(*serviceerror.Unavailable) 168 s.True(ok) 169 s.Contains(err.Error(), "ListOpenWorkflowExecutions failed") 170 } 171 172 func (s *ESVisibilitySuite) TestListOpenWorkflowExecutionsWithNamespaceDivision() { 173 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 174 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 175 source, _ := input.Query.Source() 176 s.Contains(fmt.Sprintf("%v", source), filterOpen) 177 s.Contains(fmt.Sprintf("%v", source), filterByNSDivision) 178 return testSearchResult, nil 179 }) 180 _, err := s.visibilityStore.ListOpenWorkflowExecutions(context.Background(), createTestRequestWithNSDivision()) 181 s.NoError(err) 182 } 183 184 func (s *ESVisibilitySuite) TestListClosedWorkflowExecutions() { 185 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 186 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 187 source, _ := input.Query.Source() 188 s.Regexp(filterCloseRE, fmt.Sprintf("%v", source)) 189 return testSearchResult, nil 190 }) 191 _, err := s.visibilityStore.ListClosedWorkflowExecutions(context.Background(), createTestRequest()) 192 s.NoError(err) 193 194 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 195 _, err = s.visibilityStore.ListClosedWorkflowExecutions(context.Background(), createTestRequest()) 196 s.Error(err) 197 _, ok := err.(*serviceerror.Unavailable) 198 s.True(ok) 199 s.Contains(err.Error(), "ListClosedWorkflowExecutions failed") 200 } 201 202 func (s *ESVisibilitySuite) TestListOpenWorkflowExecutionsByType() { 203 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 204 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 205 source, _ := input.Query.Source() 206 s.Contains(fmt.Sprintf("%v", source), filterOpen) 207 s.Contains(fmt.Sprintf("%v", source), filterByType) 208 return testSearchResult, nil 209 }) 210 211 testRequest := createTestRequest() 212 request := &manager.ListWorkflowExecutionsByTypeRequest{ 213 ListWorkflowExecutionsRequest: testRequest, 214 WorkflowTypeName: testWorkflowType, 215 } 216 _, err := s.visibilityStore.ListOpenWorkflowExecutionsByType(context.Background(), request) 217 s.NoError(err) 218 219 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 220 _, err = s.visibilityStore.ListOpenWorkflowExecutionsByType(context.Background(), request) 221 s.Error(err) 222 _, ok := err.(*serviceerror.Unavailable) 223 s.True(ok) 224 s.Contains(err.Error(), "ListOpenWorkflowExecutionsByType failed") 225 } 226 227 func (s *ESVisibilitySuite) TestListClosedWorkflowExecutionsByType() { 228 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 229 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 230 source, _ := input.Query.Source() 231 s.Regexp(filterCloseRE, fmt.Sprintf("%v", source)) 232 s.Contains(fmt.Sprintf("%v", source), filterByType) 233 return testSearchResult, nil 234 }) 235 236 testRequest := createTestRequest() 237 request := &manager.ListWorkflowExecutionsByTypeRequest{ 238 ListWorkflowExecutionsRequest: testRequest, 239 WorkflowTypeName: testWorkflowType, 240 } 241 _, err := s.visibilityStore.ListClosedWorkflowExecutionsByType(context.Background(), request) 242 s.NoError(err) 243 244 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 245 _, err = s.visibilityStore.ListClosedWorkflowExecutionsByType(context.Background(), request) 246 s.Error(err) 247 _, ok := err.(*serviceerror.Unavailable) 248 s.True(ok) 249 s.Contains(err.Error(), "ListClosedWorkflowExecutionsByType failed") 250 } 251 252 func (s *ESVisibilitySuite) TestListOpenWorkflowExecutionsByWorkflowID() { 253 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 254 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 255 source, _ := input.Query.Source() 256 s.Contains(fmt.Sprintf("%v", source), filterOpen) 257 s.Contains(fmt.Sprintf("%v", source), filterByWID) 258 return testSearchResult, nil 259 }) 260 261 testRequest := createTestRequest() 262 request := &manager.ListWorkflowExecutionsByWorkflowIDRequest{ 263 ListWorkflowExecutionsRequest: testRequest, 264 WorkflowID: testWorkflowID, 265 } 266 _, err := s.visibilityStore.ListOpenWorkflowExecutionsByWorkflowID(context.Background(), request) 267 s.NoError(err) 268 269 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 270 _, err = s.visibilityStore.ListOpenWorkflowExecutionsByWorkflowID(context.Background(), request) 271 s.Error(err) 272 _, ok := err.(*serviceerror.Unavailable) 273 s.True(ok) 274 s.Contains(err.Error(), "ListOpenWorkflowExecutionsByWorkflowID failed") 275 } 276 277 func (s *ESVisibilitySuite) TestListClosedWorkflowExecutionsByWorkflowID() { 278 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 279 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 280 source, _ := input.Query.Source() 281 s.Regexp(filterCloseRE, fmt.Sprintf("%v", source)) 282 s.Contains(fmt.Sprintf("%v", source), filterByWID) 283 return testSearchResult, nil 284 }) 285 286 testRequest := createTestRequest() 287 request := &manager.ListWorkflowExecutionsByWorkflowIDRequest{ 288 ListWorkflowExecutionsRequest: testRequest, 289 WorkflowID: testWorkflowID, 290 } 291 _, err := s.visibilityStore.ListClosedWorkflowExecutionsByWorkflowID(context.Background(), request) 292 s.NoError(err) 293 294 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 295 _, err = s.visibilityStore.ListClosedWorkflowExecutionsByWorkflowID(context.Background(), request) 296 s.Error(err) 297 _, ok := err.(*serviceerror.Unavailable) 298 s.True(ok) 299 s.Contains(err.Error(), "ListClosedWorkflowExecutionsByWorkflowID failed") 300 } 301 302 func (s *ESVisibilitySuite) TestListClosedWorkflowExecutionsByStatus() { 303 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 304 func(ctx context.Context, input *client.SearchParameters) (*elastic.SearchResult, error) { 305 source, _ := input.Query.Source() 306 s.Contains(fmt.Sprintf("%v", source), filterByExecutionStatus) 307 return testSearchResult, nil 308 }) 309 310 testRequest := createTestRequest() 311 request := &manager.ListClosedWorkflowExecutionsByStatusRequest{ 312 ListWorkflowExecutionsRequest: testRequest, 313 Status: testStatus, 314 } 315 _, err := s.visibilityStore.ListClosedWorkflowExecutionsByStatus(context.Background(), request) 316 s.NoError(err) 317 318 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 319 _, err = s.visibilityStore.ListClosedWorkflowExecutionsByStatus(context.Background(), request) 320 s.Error(err) 321 _, ok := err.(*serviceerror.Unavailable) 322 s.True(ok) 323 s.Contains(err.Error(), "ListClosedWorkflowExecutionsByStatus failed") 324 } 325 326 func (s *ESVisibilitySuite) TestBuildSearchParameters() { 327 request := createTestRequest() 328 329 matchNamespaceQuery := elastic.NewTermQuery(searchattribute.NamespaceID, request.NamespaceID.String()) 330 runningQuery := elastic.NewTermQuery(searchattribute.ExecutionStatus, int(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING)) 331 332 token := &visibilityPageToken{SearchAfter: []interface{}{1528358645123456789, "qwe"}} 333 var err error 334 request.NextPageToken, err = s.visibilityStore.serializePageToken(token) 335 s.NoError(err) 336 337 // test for open 338 rangeQuery := elastic.NewRangeQuery(searchattribute.StartTime).Gte(request.EarliestStartTime).Lte(request.LatestStartTime) 339 boolQuery := elastic.NewBoolQuery().Filter(runningQuery).Filter(matchNamespaceQuery).Filter(rangeQuery).MustNot(namespaceDivisionExists) 340 p, err := s.visibilityStore.buildSearchParameters(request, elastic.NewBoolQuery().Filter(elastic.NewTermQuery(searchattribute.ExecutionStatus, int(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING))), true) 341 s.NoError(err) 342 s.Equal(&client.SearchParameters{ 343 Index: testIndex, 344 Query: boolQuery, 345 SearchAfter: []interface{}{json.Number("1528358645123456789"), "qwe"}, 346 PageSize: testPageSize, 347 Sorter: defaultSorter, 348 }, p) 349 350 // test request latestTime overflow 351 request.LatestStartTime = time.Unix(0, math.MaxInt64).UTC() 352 rangeQuery = elastic.NewRangeQuery(searchattribute.StartTime).Gte(request.EarliestStartTime).Lte(request.LatestStartTime) 353 boolQuery = elastic.NewBoolQuery().Filter(runningQuery).Filter(matchNamespaceQuery).Filter(rangeQuery).MustNot(namespaceDivisionExists) 354 p, err = s.visibilityStore.buildSearchParameters(request, elastic.NewBoolQuery().Filter(elastic.NewTermQuery(searchattribute.ExecutionStatus, int(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING))), true) 355 s.NoError(err) 356 s.Equal(&client.SearchParameters{ 357 Index: testIndex, 358 Query: boolQuery, 359 SearchAfter: []interface{}{json.Number("1528358645123456789"), "qwe"}, 360 PageSize: testPageSize, 361 Sorter: defaultSorter, 362 }, p) 363 request = createTestRequest() // revert 364 365 // test for closed 366 rangeQuery = elastic.NewRangeQuery(searchattribute.CloseTime).Gte(request.EarliestStartTime).Lte(request.LatestStartTime) 367 boolQuery = elastic.NewBoolQuery().MustNot(runningQuery).Filter(matchNamespaceQuery).Filter(rangeQuery).MustNot(namespaceDivisionExists) 368 p, err = s.visibilityStore.buildSearchParameters(request, elastic.NewBoolQuery().MustNot(elastic.NewTermQuery(searchattribute.ExecutionStatus, int(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING))), false) 369 s.NoError(err) 370 s.Equal(&client.SearchParameters{ 371 Index: testIndex, 372 Query: boolQuery, 373 SearchAfter: nil, 374 PageSize: testPageSize, 375 Sorter: defaultSorter, 376 }, p) 377 378 // test for additional boolQuery 379 rangeQuery = elastic.NewRangeQuery(searchattribute.StartTime).Gte(request.EarliestStartTime).Lte(request.LatestStartTime) 380 matchQuery := elastic.NewTermQuery(searchattribute.ExecutionStatus, int32(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING)) 381 boolQuery = elastic.NewBoolQuery().Filter(matchQuery).Filter(matchNamespaceQuery).Filter(rangeQuery).MustNot(namespaceDivisionExists) 382 p, err = s.visibilityStore.buildSearchParameters(request, elastic.NewBoolQuery().Filter(matchQuery), true) 383 s.NoError(err) 384 s.Equal(&client.SearchParameters{ 385 Index: testIndex, 386 Query: boolQuery, 387 SearchAfter: nil, 388 PageSize: testPageSize, 389 Sorter: defaultSorter, 390 }, p) 391 392 // test for search after 393 token = &visibilityPageToken{SearchAfter: []interface{}{json.Number("1528358645123456789"), "qwe"}} 394 request.NextPageToken, err = s.visibilityStore.serializePageToken(token) 395 s.NoError(err) 396 397 rangeQuery = elastic.NewRangeQuery(searchattribute.StartTime).Gte(request.EarliestStartTime).Lte(request.LatestStartTime) 398 boolQuery = elastic.NewBoolQuery().Filter(matchNamespaceQuery).Filter(rangeQuery).MustNot(namespaceDivisionExists) 399 p, err = s.visibilityStore.buildSearchParameters(request, elastic.NewBoolQuery(), true) 400 s.NoError(err) 401 s.Equal(&client.SearchParameters{ 402 Index: testIndex, 403 Query: boolQuery, 404 SearchAfter: token.SearchAfter, 405 PageSize: testPageSize, 406 Sorter: defaultSorter, 407 }, p) 408 request = createTestRequest() // revert 409 410 // test for nil token 411 rangeQuery = elastic.NewRangeQuery(searchattribute.StartTime).Gte(request.EarliestStartTime).Lte(request.LatestStartTime) 412 boolQuery = elastic.NewBoolQuery().Filter(matchNamespaceQuery).Filter(rangeQuery).MustNot(namespaceDivisionExists) 413 request.NextPageToken = nil 414 p, err = s.visibilityStore.buildSearchParameters(request, elastic.NewBoolQuery(), true) 415 s.NoError(err) 416 s.Equal(&client.SearchParameters{ 417 Index: testIndex, 418 Query: boolQuery, 419 PageSize: testPageSize, 420 SearchAfter: nil, 421 Sorter: defaultSorter, 422 }, p) 423 } 424 425 func (s *ESVisibilitySuite) TestGetListFieldSorter() { 426 427 // test defaultSorter is returned when fieldSorts is empty 428 fieldSorts := make([]elastic.Sorter, 0) 429 sorter, err := s.visibilityStore.getListFieldSorter(fieldSorts) 430 s.NoError(err) 431 s.Equal(defaultSorter, sorter) 432 433 // test passing non-empty fieldSorts 434 testFieldSorts := []elastic.Sorter{elastic.NewFieldSort("_test"), elastic.NewFieldSort("_second_tes")} 435 sorter, err = s.visibilityStore.getListFieldSorter(testFieldSorts[:]) 436 expectedSorter := make([]elastic.Sorter, len(testFieldSorts)+1) 437 expectedSorter[0] = testFieldSorts[0] 438 expectedSorter[1] = testFieldSorts[1] 439 expectedSorter[2] = elastic.NewFieldSort(searchattribute.RunID).Desc() 440 s.NoError(err) 441 s.Equal(expectedSorter, sorter) 442 443 } 444 445 func (s *ESVisibilitySuite) TestGetScanFieldSorter() { 446 // test docSorter is returned when fieldSorts is empty 447 fieldSorts := make([]elastic.Sorter, 0) 448 sorter, err := s.visibilityStore.getScanFieldSorter(fieldSorts) 449 s.NoError(err) 450 s.Equal(docSorter, sorter) 451 452 // test error is returned if fieldSorts is not empty 453 testFieldSorts := []elastic.Sorter{elastic.NewFieldSort("_test"), elastic.NewFieldSort("_second_tes")} 454 sorter, err = s.visibilityStore.getScanFieldSorter(testFieldSorts[:]) 455 s.Error(err) 456 s.Nil(sorter) 457 } 458 459 func (s *ESVisibilitySuite) TestBuildSearchParametersV2() { 460 request := &manager.ListWorkflowExecutionsRequestV2{ 461 NamespaceID: testNamespaceID, 462 Namespace: testNamespace, 463 PageSize: testPageSize, 464 } 465 466 matchNamespaceQuery := elastic.NewTermQuery(searchattribute.NamespaceID, request.NamespaceID.String()) 467 matchNSDivision := elastic.NewMatchQuery(searchattribute.TemporalNamespaceDivision, "hidden-stuff") 468 469 // test for open 470 request.Query = `WorkflowId="guid-2208"` 471 filterQuery := elastic.NewBoolQuery().Filter(elastic.NewMatchQuery(searchattribute.WorkflowID, "guid-2208")) 472 boolQuery := elastic.NewBoolQuery().Filter(matchNamespaceQuery, filterQuery).MustNot(namespaceDivisionExists) 473 p, err := s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getListFieldSorter) 474 s.NoError(err) 475 s.Equal(&client.SearchParameters{ 476 Index: testIndex, 477 Query: boolQuery, 478 SearchAfter: nil, 479 PointInTime: nil, 480 PageSize: testPageSize, 481 Sorter: defaultSorter, 482 }, p) 483 request.Query = "" 484 485 // test for open with namespace division 486 request.Query = `WorkflowId="guid-2208" and TemporalNamespaceDivision="hidden-stuff"` 487 // note namespace division appears in the filterQuery, not the boolQuery like the negative version 488 filterQuery = elastic.NewBoolQuery().Filter(elastic.NewMatchQuery(searchattribute.WorkflowID, "guid-2208"), matchNSDivision) 489 boolQuery = elastic.NewBoolQuery().Filter(matchNamespaceQuery, filterQuery) 490 p, err = s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getListFieldSorter) 491 s.NoError(err) 492 s.Equal(&client.SearchParameters{ 493 Index: testIndex, 494 Query: boolQuery, 495 SearchAfter: nil, 496 PointInTime: nil, 497 PageSize: testPageSize, 498 Sorter: defaultSorter, 499 }, p) 500 request.Query = "" 501 502 // test custom sort 503 request.Query = `Order bY WorkflowId` 504 boolQuery = elastic.NewBoolQuery().Filter(matchNamespaceQuery).MustNot(namespaceDivisionExists) 505 s.mockMetricsHandler.EXPECT().WithTags(metrics.NamespaceTag(request.Namespace.String())).Return(s.mockMetricsHandler) 506 s.mockMetricsHandler.EXPECT().Counter(metrics.ElasticsearchCustomOrderByClauseCount.Name()).Return(metrics.NoopCounterMetricFunc) 507 p, err = s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getListFieldSorter) 508 s.NoError(err) 509 s.Equal(&client.SearchParameters{ 510 Index: testIndex, 511 Query: boolQuery, 512 SearchAfter: nil, 513 PointInTime: nil, 514 PageSize: testPageSize, 515 Sorter: []elastic.Sorter{ 516 elastic.NewFieldSort(searchattribute.WorkflowID).Asc(), 517 elastic.NewFieldSort(searchattribute.RunID).Desc(), 518 }, 519 }, p) 520 request.Query = "" 521 522 // test with Scan API 523 request.Query = `WorkflowId="guid-2208"` 524 filterQuery = elastic.NewBoolQuery().Filter(elastic.NewMatchQuery(searchattribute.WorkflowID, "guid-2208")) 525 boolQuery = elastic.NewBoolQuery().Filter(matchNamespaceQuery, filterQuery).MustNot(namespaceDivisionExists) 526 p, err = s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getScanFieldSorter) 527 s.NoError(err) 528 s.Equal(&client.SearchParameters{ 529 Index: testIndex, 530 Query: boolQuery, 531 SearchAfter: nil, 532 PointInTime: nil, 533 PageSize: testPageSize, 534 Sorter: docSorter, 535 }, p) 536 request.Query = "" 537 538 // test with Scan API with custom sort 539 request.Query = `Order bY WorkflowId` 540 s.mockMetricsHandler.EXPECT().WithTags(metrics.NamespaceTag(request.Namespace.String())).Return(s.mockMetricsHandler) 541 s.mockMetricsHandler.EXPECT().Counter(metrics.ElasticsearchCustomOrderByClauseCount.Name()).Return(metrics.NoopCounterMetricFunc) 542 p, err = s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getScanFieldSorter) 543 s.Error(err) 544 s.Nil(p) 545 request.Query = "" 546 547 // test for wrong query 548 request.Query = "invalid query" 549 p, err = s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getListFieldSorter) 550 s.Nil(p) 551 s.Error(err) 552 request.Query = "" 553 } 554 555 func (s *ESVisibilitySuite) TestBuildSearchParametersV2DisableOrderByClause() { 556 request := &manager.ListWorkflowExecutionsRequestV2{ 557 NamespaceID: testNamespaceID, 558 Namespace: testNamespace, 559 PageSize: testPageSize, 560 } 561 562 matchNamespaceQuery := elastic.NewTermQuery(searchattribute.NamespaceID, request.NamespaceID.String()) 563 564 // disable ORDER BY clause 565 s.visibilityStore.disableOrderByClause = dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true) 566 567 // test valid query 568 request.Query = `WorkflowId="guid-2208"` 569 filterQuery := elastic.NewBoolQuery().Filter(elastic.NewMatchQuery(searchattribute.WorkflowID, "guid-2208")) 570 boolQuery := elastic.NewBoolQuery().Filter(matchNamespaceQuery, filterQuery).MustNot(namespaceDivisionExists) 571 p, err := s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getListFieldSorter) 572 s.NoError(err) 573 s.Equal(&client.SearchParameters{ 574 Index: testIndex, 575 Query: boolQuery, 576 SearchAfter: nil, 577 PointInTime: nil, 578 PageSize: testPageSize, 579 Sorter: defaultSorter, 580 }, p) 581 request.Query = "" 582 583 // test invalid query with ORDER BY 584 request.Query = `ORDER BY WorkflowId` 585 p, err = s.visibilityStore.buildSearchParametersV2(request, s.visibilityStore.getListFieldSorter) 586 s.Nil(p) 587 s.Error(err) 588 var invalidArgumentErr *serviceerror.InvalidArgument 589 s.ErrorAs(err, &invalidArgumentErr) 590 s.EqualError(err, "ORDER BY clause is not supported") 591 request.Query = "" 592 } 593 594 func (s *ESVisibilitySuite) queryToJSON(q elastic.Query) string { 595 m, err := q.Source() 596 s.NoError(err) 597 b, err := json.Marshal(m) 598 s.NoError(err) 599 return string(b) 600 } 601 602 func (s *ESVisibilitySuite) sorterToJSON(sorters []elastic.Sorter) string { 603 var ms []interface{} 604 for _, sorter := range sorters { 605 m, err := sorter.Source() 606 s.NoError(err) 607 ms = append(ms, m) 608 } 609 b, err := json.Marshal(ms) 610 s.NoError(err) 611 return string(b) 612 } 613 614 func (s *ESVisibilitySuite) Test_convertQuery() { 615 query := `WorkflowId = 'wid'` 616 queryParams, err := s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 617 s.NoError(err) 618 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"match":{"WorkflowId":{"query":"wid"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 619 s.Nil(queryParams.Sorter) 620 621 query = `WorkflowId = 'wid' or WorkflowId = 'another-wid'` 622 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 623 s.NoError(err) 624 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"should":[{"match":{"WorkflowId":{"query":"wid"}}},{"match":{"WorkflowId":{"query":"another-wid"}}}]}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 625 s.Nil(queryParams.Sorter) 626 627 query = `WorkflowId = 'wid' order by StartTime desc` 628 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 629 s.NoError(err) 630 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"match":{"WorkflowId":{"query":"wid"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 631 s.Equal(`[{"StartTime":{"order":"desc"}}]`, s.sorterToJSON(queryParams.Sorter)) 632 633 query = `WorkflowId = 'wid' and CloseTime is null` 634 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 635 s.NoError(err) 636 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":[{"match":{"WorkflowId":{"query":"wid"}}},{"bool":{"must_not":{"exists":{"field":"CloseTime"}}}}]}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 637 s.Nil(queryParams.Sorter) 638 639 query = `WorkflowId = 'wid' or CloseTime is null` 640 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 641 s.NoError(err) 642 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"should":[{"match":{"WorkflowId":{"query":"wid"}}},{"bool":{"must_not":{"exists":{"field":"CloseTime"}}}}]}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 643 s.Nil(queryParams.Sorter) 644 645 query = `CloseTime is null order by CloseTime desc` 646 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 647 s.NoError(err) 648 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"must_not":{"exists":{"field":"CloseTime"}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 649 s.Equal(`[{"CloseTime":{"order":"desc"}}]`, s.sorterToJSON(queryParams.Sorter)) 650 651 query = `StartTime = "2018-06-07T15:04:05.123456789-08:00"` 652 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 653 s.NoError(err) 654 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"match":{"StartTime":{"query":"2018-06-07T15:04:05.123456789-08:00"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 655 s.Nil(queryParams.Sorter) 656 657 query = `WorkflowId = 'wid' and StartTime > "2018-06-07T15:04:05+00:00"` 658 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 659 s.NoError(err) 660 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":[{"match":{"WorkflowId":{"query":"wid"}}},{"range":{"StartTime":{"from":"2018-06-07T15:04:05+00:00","include_lower":false,"include_upper":true,"to":null}}}]}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 661 s.Nil(queryParams.Sorter) 662 663 query = `ExecutionTime < 1000000` 664 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 665 s.NoError(err) 666 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"range":{"ExecutionTime":{"from":null,"include_lower":true,"include_upper":false,"to":"1970-01-01T00:00:00.001Z"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 667 s.Nil(queryParams.Sorter) 668 669 query = `ExecutionTime between 1 and 2` 670 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 671 s.NoError(err) 672 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"range":{"ExecutionTime":{"from":"1970-01-01T00:00:00.000000001Z","include_lower":true,"include_upper":true,"to":"1970-01-01T00:00:00.000000002Z"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 673 s.Nil(queryParams.Sorter) 674 675 query = `ExecutionTime < 1000000 or ExecutionTime > 2000000` 676 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 677 s.NoError(err) 678 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"should":[{"range":{"ExecutionTime":{"from":null,"include_lower":true,"include_upper":false,"to":"1970-01-01T00:00:00.001Z"}}},{"range":{"ExecutionTime":{"from":"1970-01-01T00:00:00.002Z","include_lower":false,"include_upper":true,"to":null}}}]}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 679 s.Nil(queryParams.Sorter) 680 681 query = `order by ExecutionTime` 682 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 683 s.NoError(err) 684 s.Equal(`{"bool":{"filter":{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 685 s.Equal(`[{"ExecutionTime":{"order":"asc"}}]`, s.sorterToJSON(queryParams.Sorter)) 686 687 query = `order by StartTime desc, CloseTime asc` 688 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 689 s.NoError(err) 690 s.Equal(`{"bool":{"filter":{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 691 s.Equal(`[{"StartTime":{"order":"desc"}},{"CloseTime":{"order":"asc"}}]`, s.sorterToJSON(queryParams.Sorter)) 692 693 query = `order by CustomTextField desc` 694 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 695 s.Error(err) 696 s.IsType(&serviceerror.InvalidArgument{}, err) 697 s.Equal(err.(*serviceerror.InvalidArgument).Error(), "invalid query: unable to convert 'order by' column name: unable to sort by field of Text type, use field of type Keyword") 698 699 query = `order by CustomIntField asc` 700 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 701 s.NoError(err) 702 s.Equal(`{"bool":{"filter":{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 703 s.Equal(`[{"CustomIntField":{"order":"asc"}}]`, s.sorterToJSON(queryParams.Sorter)) 704 705 query = `ExecutionTime < "unable to parse"` 706 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 707 s.Error(err) 708 s.IsType(&serviceerror.InvalidArgument{}, err) 709 s.Equal(err.Error(), "invalid query: unable to convert filter expression: unable to convert values of comparison expression: invalid value for search attribute ExecutionTime of type Datetime: \"unable to parse\"") 710 711 // invalid union injection 712 query = `WorkflowId = 'wid' union select * from dummy` 713 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 714 s.Error(err) 715 s.Nil(queryParams) 716 } 717 718 func (s *ESVisibilitySuite) Test_convertQuery_Mapper() { 719 s.mockSearchAttributesMapperProvider.EXPECT().GetMapper(testNamespace). 720 Return(&searchattribute.TestMapper{}, nil).AnyTimes() 721 722 s.visibilityStore.searchAttributesMapperProvider = s.mockSearchAttributesMapperProvider 723 724 query := `WorkflowId = 'wid'` 725 queryParams, err := s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 726 s.NoError(err) 727 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"match":{"WorkflowId":{"query":"wid"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 728 s.Nil(queryParams.Sorter) 729 730 query = `AliasForCustomKeywordField = 'pid'` 731 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 732 s.NoError(err) 733 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"match":{"CustomKeywordField":{"query":"pid"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 734 s.Nil(queryParams.Sorter) 735 736 query = `CustomKeywordField = 'pid'` 737 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 738 s.Error(err) 739 var invalidArgumentErr *serviceerror.InvalidArgument 740 s.ErrorAs(err, &invalidArgumentErr) 741 s.EqualError(err, "mapper error") 742 743 query = `AliasForUnknownField = 'pid'` 744 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 745 s.Error(err) 746 s.ErrorAs(err, &invalidArgumentErr) 747 s.EqualError(err, "invalid query: unable to convert filter expression: unable to convert left side of \"AliasForUnknownField = 'pid'\": invalid search attribute: AliasForUnknownField") 748 749 query = `order by ExecutionTime` 750 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 751 s.NoError(err) 752 s.Equal(`{"bool":{"filter":{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 753 s.Equal(`[{"ExecutionTime":{"order":"asc"}}]`, s.sorterToJSON(queryParams.Sorter)) 754 755 query = `order by AliasForCustomKeywordField asc` 756 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 757 s.NoError(err) 758 s.Equal(`{"bool":{"filter":{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 759 s.Equal(`[{"CustomKeywordField":{"order":"asc"}}]`, s.sorterToJSON(queryParams.Sorter)) 760 761 query = `order by CustomKeywordField asc` 762 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 763 s.Error(err) 764 s.ErrorAs(err, &invalidArgumentErr) 765 s.EqualError(err, "mapper error") 766 767 query = `order by AliasForUnknownField asc` 768 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 769 s.Error(err) 770 s.ErrorAs(err, &invalidArgumentErr) 771 s.EqualError(err, "invalid query: unable to convert 'order by' column name: invalid search attribute: AliasForUnknownField") 772 s.visibilityStore.searchAttributesMapperProvider = nil 773 } 774 775 func (s *ESVisibilitySuite) Test_convertQuery_Mapper_Error() { 776 s.mockSearchAttributesMapperProvider.EXPECT().GetMapper(testNamespace). 777 Return(&searchattribute.TestMapper{}, nil).AnyTimes() 778 779 s.visibilityStore.searchAttributesMapperProvider = s.mockSearchAttributesMapperProvider 780 781 query := `WorkflowId = 'wid'` 782 queryParams, err := s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 783 s.NoError(err) 784 s.Equal(`{"bool":{"filter":[{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},{"bool":{"filter":{"match":{"WorkflowId":{"query":"wid"}}}}}],"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 785 s.Nil(queryParams.Sorter) 786 787 query = `ProductId = 'pid'` 788 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 789 s.Error(err) 790 var invalidArgumentErr *serviceerror.InvalidArgument 791 s.ErrorAs(err, &invalidArgumentErr) 792 s.EqualError(err, "mapper error") 793 794 query = `order by ExecutionTime` 795 queryParams, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 796 s.NoError(err) 797 s.Equal(`{"bool":{"filter":{"term":{"NamespaceId":"bfd5c907-f899-4baf-a7b2-2ab85e623ebd"}},"must_not":{"exists":{"field":"TemporalNamespaceDivision"}}}}`, s.queryToJSON(queryParams.Query)) 798 s.Equal(`[{"ExecutionTime":{"order":"asc"}}]`, s.sorterToJSON(queryParams.Sorter)) 799 800 query = `order by CustomIntField asc` 801 _, err = s.visibilityStore.convertQuery(testNamespace, testNamespaceID, query) 802 s.Error(err) 803 s.ErrorAs(err, &invalidArgumentErr) 804 s.EqualError(err, "mapper error") 805 806 s.visibilityStore.searchAttributesMapperProvider = nil 807 } 808 809 func (s *ESVisibilitySuite) TestGetListWorkflowExecutionsResponse() { 810 // test for empty hits 811 searchResult := &elastic.SearchResult{ 812 Hits: &elastic.SearchHits{ 813 TotalHits: &elastic.TotalHits{}, 814 }} 815 resp, err := s.visibilityStore.getListWorkflowExecutionsResponse(searchResult, testNamespace, 1) 816 s.NoError(err) 817 s.Equal(0, len(resp.NextPageToken)) 818 s.Equal(0, len(resp.Executions)) 819 820 // test for one hits 821 data := []byte(`{"ExecutionStatus": "Running", 822 "CloseTime": "2021-06-11T16:04:07.980-07:00", 823 "NamespaceId": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 824 "HistoryLength": 29, 825 "StateTransitionCount": 22, 826 "VisibilityTaskKey": "7-619", 827 "RunId": "e481009e-14b3-45ae-91af-dce6e2a88365", 828 "StartTime": "2021-06-11T15:04:07.980-07:00", 829 "WorkflowId": "6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", 830 "WorkflowType": "basic.stressWorkflowExecute"}`) 831 source := json.RawMessage(data) 832 searchHit := &elastic.SearchHit{ 833 Source: source, 834 Sort: []interface{}{1547596872371234567, "e481009e-14b3-45ae-91af-dce6e2a88365"}, 835 } 836 searchResult.Hits.Hits = []*elastic.SearchHit{searchHit} 837 searchResult.Hits.TotalHits.Value = 1 838 resp, err = s.visibilityStore.getListWorkflowExecutionsResponse(searchResult, testNamespace, 1) 839 s.NoError(err) 840 serializedToken, _ := s.visibilityStore.serializePageToken(&visibilityPageToken{SearchAfter: []interface{}{1547596872371234567, "e481009e-14b3-45ae-91af-dce6e2a88365"}}) 841 s.Equal(serializedToken, resp.NextPageToken) 842 s.Equal(1, len(resp.Executions)) 843 844 // test for last page hits 845 resp, err = s.visibilityStore.getListWorkflowExecutionsResponse(searchResult, testNamespace, 2) 846 s.NoError(err) 847 s.Equal(0, len(resp.NextPageToken)) 848 s.Equal(1, len(resp.Executions)) 849 850 // test for search after 851 searchResult.Hits.Hits = []*elastic.SearchHit{} 852 for i := int64(0); i < searchResult.Hits.TotalHits.Value; i++ { 853 searchResult.Hits.Hits = append(searchResult.Hits.Hits, searchHit) 854 } 855 numOfHits := len(searchResult.Hits.Hits) 856 resp, err = s.visibilityStore.getListWorkflowExecutionsResponse(searchResult, testNamespace, numOfHits) 857 s.NoError(err) 858 s.Equal(numOfHits, len(resp.Executions)) 859 nextPageToken, err := s.visibilityStore.deserializePageToken(resp.NextPageToken) 860 s.NoError(err) 861 resultSortValue, err := nextPageToken.SearchAfter[0].(json.Number).Int64() 862 s.NoError(err) 863 s.Equal(int64(1547596872371234567), resultSortValue) 864 s.Equal("e481009e-14b3-45ae-91af-dce6e2a88365", nextPageToken.SearchAfter[1]) 865 // for last page 866 resp, err = s.visibilityStore.getListWorkflowExecutionsResponse(searchResult, testNamespace, numOfHits+1) 867 s.NoError(err) 868 s.Equal(0, len(resp.NextPageToken)) 869 s.Equal(numOfHits, len(resp.Executions)) 870 } 871 872 func (s *ESVisibilitySuite) TestDeserializePageToken() { 873 badInput := []byte("bad input") 874 result, err := s.visibilityStore.deserializePageToken(badInput) 875 s.Error(err) 876 s.Nil(result) 877 err, ok := err.(*serviceerror.InvalidArgument) 878 s.True(ok) 879 s.Contains(err.Error(), "unable to deserialize page token") 880 881 result, err = s.visibilityStore.deserializePageToken(nil) 882 s.NoError(err) 883 s.Nil(result) 884 885 token := &visibilityPageToken{SearchAfter: []interface{}{int64(1629936710090695939), "unique"}} 886 data, err := s.visibilityStore.serializePageToken(token) 887 s.NoError(err) 888 result, err = s.visibilityStore.deserializePageToken(data) 889 s.NoError(err) 890 resultSortValue, err := result.SearchAfter[0].(json.Number).Int64() 891 s.NoError(err) 892 s.Equal(token.SearchAfter[0].(int64), resultSortValue) 893 } 894 895 func (s *ESVisibilitySuite) TestSerializePageToken() { 896 data, err := s.visibilityStore.serializePageToken(nil) 897 s.NoError(err) 898 s.Nil(data) 899 token, err := s.visibilityStore.deserializePageToken(data) 900 s.NoError(err) 901 s.Nil(token) 902 903 sortTime := int64(123) 904 tieBreaker := "unique" 905 newToken := &visibilityPageToken{SearchAfter: []interface{}{sortTime, tieBreaker}} 906 data, err = s.visibilityStore.serializePageToken(newToken) 907 s.NoError(err) 908 s.True(len(data) > 0) 909 token, err = s.visibilityStore.deserializePageToken(data) 910 s.NoError(err) 911 resultSortValue, err := token.SearchAfter[0].(json.Number).Int64() 912 s.NoError(err) 913 s.Equal(newToken.SearchAfter[0], resultSortValue) 914 s.Equal(newToken.SearchAfter[1], token.SearchAfter[1]) 915 } 916 917 func (s *ESVisibilitySuite) TestParseESDoc() { 918 docSource := []byte(`{"ExecutionStatus": "Running", 919 "NamespaceId": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 920 "HistoryLength": 29, 921 "StateTransitionCount": 10, 922 "VisibilityTaskKey": "7-619", 923 "RunId": "e481009e-14b3-45ae-91af-dce6e2a88365", 924 "StartTime": "2021-06-11T15:04:07.980-07:00", 925 "WorkflowId": "6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", 926 "WorkflowType": "TestWorkflowExecute"}`) 927 // test for open 928 info, err := s.visibilityStore.parseESDoc("", docSource, searchattribute.TestNameTypeMap, testNamespace) 929 s.NoError(err) 930 s.NotNil(info) 931 s.Equal("6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", info.WorkflowID) 932 s.Equal("e481009e-14b3-45ae-91af-dce6e2a88365", info.RunID) 933 s.Equal("TestWorkflowExecute", info.TypeName) 934 s.Equal(int64(10), info.StateTransitionCount) 935 s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.Status) 936 expectedStartTime, err := time.Parse(time.RFC3339Nano, "2021-06-11T15:04:07.980-07:00") 937 s.NoError(err) 938 s.Equal(expectedStartTime, info.StartTime) 939 s.Nil(info.SearchAttributes) 940 941 // test for close 942 docSource = []byte(`{"ExecutionStatus": "Completed", 943 "CloseTime": "2021-06-11T16:04:07Z", 944 "NamespaceId": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 945 "HistoryLength": 29, 946 "StateTransitionCount": 20, 947 "VisibilityTaskKey": "7-619", 948 "RunId": "e481009e-14b3-45ae-91af-dce6e2a88365", 949 "StartTime": "2021-06-11T15:04:07.980-07:00", 950 "WorkflowId": "6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", 951 "WorkflowType": "TestWorkflowExecute"}`) 952 info, err = s.visibilityStore.parseESDoc("", docSource, searchattribute.TestNameTypeMap, testNamespace) 953 s.NoError(err) 954 s.NotNil(info) 955 s.Equal("6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", info.WorkflowID) 956 s.Equal("e481009e-14b3-45ae-91af-dce6e2a88365", info.RunID) 957 s.Equal("TestWorkflowExecute", info.TypeName) 958 s.Equal(int64(20), info.StateTransitionCount) 959 expectedStartTime, err = time.Parse(time.RFC3339Nano, "2021-06-11T15:04:07.980-07:00") 960 s.NoError(err) 961 expectedCloseTime, err := time.Parse(time.RFC3339Nano, "2021-06-11T16:04:07Z") 962 s.NoError(err) 963 s.Equal(expectedStartTime, info.StartTime) 964 s.Equal(expectedCloseTime, info.CloseTime) 965 s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, info.Status) 966 s.Equal(int64(29), info.HistoryLength) 967 s.Nil(info.SearchAttributes) 968 969 // test for error case 970 docSource = []byte(`corrupted data`) 971 s.mockMetricsHandler.EXPECT().Counter(metrics.ElasticsearchDocumentParseFailuresCount.Name()).Return(metrics.NoopCounterMetricFunc) 972 info, err = s.visibilityStore.parseESDoc("", docSource, searchattribute.TestNameTypeMap, testNamespace) 973 s.Error(err) 974 s.Nil(info) 975 } 976 977 func (s *ESVisibilitySuite) TestParseESDoc_SearchAttributes() { 978 docSource := []byte(`{"ExecutionStatus": "Completed", 979 "TemporalChangeVersion": ["ver1", "ver2"], 980 "CustomKeywordField": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 981 "CustomTextField": "text text", 982 "CustomDatetimeField": ["2014-08-28T03:15:00.000-07:00", "2016-04-21T05:00:00.000-07:00"], 983 "CustomDoubleField": [1234.1234,5678.5678], 984 "CustomIntField": [111,222], 985 "CustomBoolField": true, 986 "UnknownField": "random"}`) 987 info, err := s.visibilityStore.parseESDoc("", docSource, searchattribute.TestNameTypeMap, testNamespace) 988 s.NoError(err) 989 s.NotNil(info) 990 customSearchAttributes, err := searchattribute.Decode(info.SearchAttributes, &searchattribute.TestNameTypeMap, true) 991 s.NoError(err) 992 993 s.Len(customSearchAttributes, 7) 994 995 s.Equal([]string{"ver1", "ver2"}, customSearchAttributes["TemporalChangeVersion"]) 996 997 s.Equal("bfd5c907-f899-4baf-a7b2-2ab85e623ebd", customSearchAttributes["CustomKeywordField"]) 998 999 s.Equal("text text", customSearchAttributes["CustomTextField"]) 1000 1001 date1, err := time.Parse(time.RFC3339Nano, "2014-08-28T03:15:00.000-07:00") 1002 s.NoError(err) 1003 date2, err := time.Parse(time.RFC3339Nano, "2016-04-21T05:00:00.000-07:00") 1004 s.NoError(err) 1005 s.Equal([]time.Time{date1, date2}, customSearchAttributes["CustomDatetimeField"]) 1006 1007 s.Equal([]float64{1234.1234, 5678.5678}, customSearchAttributes["CustomDoubleField"]) 1008 1009 s.Equal(true, customSearchAttributes["CustomBoolField"]) 1010 1011 s.Equal([]int64{int64(111), int64(222)}, customSearchAttributes["CustomIntField"]) 1012 1013 _, ok := customSearchAttributes["UnknownField"] 1014 s.False(ok) 1015 1016 s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, info.Status) 1017 } 1018 1019 func (s *ESVisibilitySuite) TestParseESDoc_SearchAttributes_WithMapper() { 1020 docSource := []byte(`{"ExecutionStatus": "Completed", 1021 "TemporalChangeVersion": ["ver1", "ver2"], 1022 "CustomKeywordField": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 1023 "CustomTextField": "text text", 1024 "CustomDatetimeField": ["2014-08-28T03:15:00.000-07:00", "2016-04-21T05:00:00.000-07:00"], 1025 "CustomDoubleField": [1234.1234,5678.5678], 1026 "CustomIntField": [111,222], 1027 "CustomBoolField": true, 1028 "UnknownField": "random"}`) 1029 s.visibilityStore.searchAttributesMapperProvider = s.mockSearchAttributesMapperProvider 1030 1031 s.mockSearchAttributesMapperProvider.EXPECT().GetMapper(testNamespace). 1032 Return(&searchattribute.TestMapper{}, nil).AnyTimes() 1033 1034 info, err := s.visibilityStore.parseESDoc("", docSource, searchattribute.TestNameTypeMap, testNamespace) 1035 s.NoError(err) 1036 s.NotNil(info) 1037 1038 s.Len(info.SearchAttributes.GetIndexedFields(), 7) 1039 s.Contains(info.SearchAttributes.GetIndexedFields(), "TemporalChangeVersion") 1040 s.Contains(info.SearchAttributes.GetIndexedFields(), "AliasForCustomKeywordField") 1041 s.Contains(info.SearchAttributes.GetIndexedFields(), "AliasForCustomTextField") 1042 s.Contains(info.SearchAttributes.GetIndexedFields(), "AliasForCustomDatetimeField") 1043 s.Contains(info.SearchAttributes.GetIndexedFields(), "AliasForCustomDoubleField") 1044 s.Contains(info.SearchAttributes.GetIndexedFields(), "AliasForCustomBoolField") 1045 s.Contains(info.SearchAttributes.GetIndexedFields(), "AliasForCustomIntField") 1046 s.NotContains(info.SearchAttributes.GetIndexedFields(), "UnknownField") 1047 s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, info.Status) 1048 1049 s.visibilityStore.searchAttributesMapperProvider = nil 1050 } 1051 1052 func (s *ESVisibilitySuite) TestListWorkflowExecutions() { 1053 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 1054 func(ctx context.Context, p *client.SearchParameters) (*elastic.SearchResult, error) { 1055 s.Equal(testIndex, p.Index) 1056 s.Equal( 1057 elastic.NewBoolQuery().Filter( 1058 elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String()), 1059 elastic.NewBoolQuery().Filter(elastic.NewMatchQuery("ExecutionStatus", "Terminated")), 1060 ).MustNot(namespaceDivisionExists), 1061 p.Query, 1062 ) 1063 return testSearchResult, nil 1064 }) 1065 1066 request := &manager.ListWorkflowExecutionsRequestV2{ 1067 NamespaceID: testNamespaceID, 1068 Namespace: testNamespace, 1069 PageSize: 10, 1070 Query: `ExecutionStatus = "Terminated"`, 1071 } 1072 _, err := s.visibilityStore.ListWorkflowExecutions(context.Background(), request) 1073 s.NoError(err) 1074 1075 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 1076 _, err = s.visibilityStore.ListWorkflowExecutions(context.Background(), request) 1077 s.Error(err) 1078 _, ok := err.(*serviceerror.Unavailable) 1079 s.True(ok) 1080 s.Contains(err.Error(), "ListWorkflowExecutions failed") 1081 1082 request.Query = `invalid query` 1083 _, err = s.visibilityStore.ListWorkflowExecutions(context.Background(), request) 1084 s.Error(err) 1085 _, ok = err.(*serviceerror.InvalidArgument) 1086 s.True(ok) 1087 s.True(strings.HasPrefix(err.Error(), "invalid query")) 1088 } 1089 1090 func (s *ESVisibilitySuite) TestListWorkflowExecutions_Error() { 1091 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 1092 func(ctx context.Context, p *client.SearchParameters) (*elastic.SearchResult, error) { 1093 return nil, &elastic.Error{ 1094 Status: 400, 1095 Details: &elastic.ErrorDetails{ 1096 Reason: "error reason", 1097 }, 1098 } 1099 }) 1100 1101 request := &manager.ListWorkflowExecutionsRequestV2{ 1102 NamespaceID: testNamespaceID, 1103 Namespace: testNamespace, 1104 PageSize: 10, 1105 Query: `ExecutionStatus = "Terminated"`, 1106 } 1107 _, err := s.visibilityStore.ListWorkflowExecutions(context.Background(), request) 1108 s.Error(err) 1109 var invalidArgErr *serviceerror.InvalidArgument 1110 s.ErrorAs(err, &invalidArgErr) 1111 s.Equal("ListWorkflowExecutions failed: elastic: Error 400 (Bad Request): error reason [type=]", invalidArgErr.Message) 1112 1113 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn( 1114 func(ctx context.Context, p *client.SearchParameters) (*elastic.SearchResult, error) { 1115 return nil, &elastic.Error{ 1116 Status: 500, 1117 Details: &elastic.ErrorDetails{ 1118 Reason: "error reason", 1119 }, 1120 } 1121 }) 1122 _, err = s.visibilityStore.ListWorkflowExecutions(context.Background(), request) 1123 var unavailableErr *serviceerror.Unavailable 1124 s.ErrorAs(err, &unavailableErr) 1125 s.Equal("ListWorkflowExecutions failed: elastic: Error 500 (Internal Server Error): error reason [type=]", unavailableErr.Message) 1126 } 1127 1128 func (s *ESVisibilitySuite) TestScanWorkflowExecutions_Scroll() { 1129 scrollID := "scrollID" 1130 request := &manager.ListWorkflowExecutionsRequestV2{ 1131 NamespaceID: testNamespaceID, 1132 Namespace: testNamespace, 1133 PageSize: 1, 1134 } 1135 1136 data := []byte(`{"ExecutionStatus": "Running", 1137 "CloseTime": "2021-06-11T16:04:07.980-07:00", 1138 "NamespaceId": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 1139 "HistoryLength": 29, 1140 "StateTransitionCount": 22, 1141 "VisibilityTaskKey": "7-619", 1142 "RunId": "e481009e-14b3-45ae-91af-dce6e2a88365", 1143 "StartTime": "2021-06-11T15:04:07.980-07:00", 1144 "WorkflowId": "6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", 1145 "WorkflowType": "basic.stressWorkflowExecute"}`) 1146 source := json.RawMessage(data) 1147 searchResult := &elastic.SearchResult{ 1148 Hits: &elastic.SearchHits{ 1149 Hits: []*elastic.SearchHit{ 1150 { 1151 Source: source, 1152 }, 1153 }, 1154 }, 1155 ScrollId: scrollID, 1156 } 1157 1158 s.mockESClient.EXPECT().IsPointInTimeSupported(gomock.Any()).Return(false).AnyTimes() 1159 1160 // test bad request 1161 request.Query = `invalid query` 1162 _, err := s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1163 s.Error(err) 1164 _, ok := err.(*serviceerror.InvalidArgument) 1165 s.True(ok) 1166 s.True(strings.HasPrefix(err.Error(), "invalid query")) 1167 1168 // test search 1169 request.Query = `ExecutionStatus = "Running"` 1170 s.mockESClient.EXPECT().OpenScroll( 1171 gomock.Any(), 1172 &client.SearchParameters{ 1173 Index: testIndex, 1174 Query: elastic.NewBoolQuery(). 1175 Filter( 1176 elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String()), 1177 elastic.NewBoolQuery().Filter( 1178 elastic.NewMatchQuery( 1179 searchattribute.ExecutionStatus, 1180 enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String(), 1181 ), 1182 ), 1183 ). 1184 MustNot(elastic.NewExistsQuery(searchattribute.TemporalNamespaceDivision)), 1185 PageSize: 1, 1186 Sorter: docSorter, 1187 }, 1188 gomock.Any(), 1189 ).Return(searchResult, nil) 1190 1191 token := &visibilityPageToken{ScrollID: scrollID} 1192 tokenBytes, err := s.visibilityStore.serializePageToken(token) 1193 s.NoError(err) 1194 1195 result, err := s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1196 s.NoError(err) 1197 s.Equal(tokenBytes, result.NextPageToken) 1198 1199 // test last page 1200 request.NextPageToken = tokenBytes 1201 searchResult = &elastic.SearchResult{ 1202 Hits: &elastic.SearchHits{ 1203 Hits: []*elastic.SearchHit{}, 1204 }, 1205 ScrollId: scrollID, 1206 } 1207 s.mockESClient.EXPECT().Scroll(gomock.Any(), scrollID, gomock.Any()).Return(searchResult, nil) 1208 s.mockESClient.EXPECT().CloseScroll(gomock.Any(), scrollID).Return(nil) 1209 result, err = s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1210 s.NoError(err) 1211 s.Nil(result.NextPageToken) 1212 1213 // test unavailable error 1214 request.NextPageToken = nil 1215 s.mockESClient.EXPECT().OpenScroll(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 1216 _, err = s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1217 s.Error(err) 1218 _, ok = err.(*serviceerror.Unavailable) 1219 s.True(ok) 1220 s.Contains(err.Error(), "ScanWorkflowExecutions failed") 1221 } 1222 1223 func (s *ESVisibilitySuite) TestScanWorkflowExecutions_Pit() { 1224 pitID := "pitID" 1225 request := &manager.ListWorkflowExecutionsRequestV2{ 1226 NamespaceID: testNamespaceID, 1227 Namespace: testNamespace, 1228 PageSize: 1, 1229 } 1230 1231 data := []byte(`{"ExecutionStatus": "Running", 1232 "CloseTime": "2021-06-11T16:04:07.980-07:00", 1233 "NamespaceId": "bfd5c907-f899-4baf-a7b2-2ab85e623ebd", 1234 "HistoryLength": 29, 1235 "StateTransitionCount": 22, 1236 "VisibilityTaskKey": "7-619", 1237 "RunId": "e481009e-14b3-45ae-91af-dce6e2a88365", 1238 "StartTime": "2021-06-11T15:04:07.980-07:00", 1239 "WorkflowId": "6bfbc1e5-6ce4-4e22-bbfb-e0faa9a7a604-1-2256", 1240 "WorkflowType": "basic.stressWorkflowExecute"}`) 1241 source := json.RawMessage(data) 1242 searchAfter := []any{json.Number("123")} 1243 searchResult := &elastic.SearchResult{ 1244 Hits: &elastic.SearchHits{ 1245 Hits: []*elastic.SearchHit{ 1246 { 1247 Source: source, 1248 Sort: searchAfter, 1249 }, 1250 }, 1251 }, 1252 PitId: pitID, 1253 } 1254 1255 s.mockESClient.EXPECT().IsPointInTimeSupported(gomock.Any()).Return(true).AnyTimes() 1256 1257 // test bad request 1258 request.Query = `invalid query` 1259 _, err := s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1260 s.Error(err) 1261 _, ok := err.(*serviceerror.InvalidArgument) 1262 s.True(ok) 1263 s.True(strings.HasPrefix(err.Error(), "invalid query")) 1264 1265 request.Query = `ExecutionStatus = "Running"` 1266 s.mockESClient.EXPECT().OpenPointInTime(gomock.Any(), testIndex, gomock.Any()).Return(pitID, nil) 1267 s.mockESClient.EXPECT().Search( 1268 gomock.Any(), 1269 &client.SearchParameters{ 1270 Index: testIndex, 1271 Query: elastic.NewBoolQuery(). 1272 Filter( 1273 elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String()), 1274 elastic.NewBoolQuery().Filter( 1275 elastic.NewMatchQuery( 1276 searchattribute.ExecutionStatus, 1277 enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String(), 1278 ), 1279 ), 1280 ). 1281 MustNot(elastic.NewExistsQuery(searchattribute.TemporalNamespaceDivision)), 1282 PageSize: 1, 1283 Sorter: docSorter, 1284 PointInTime: elastic.NewPointInTimeWithKeepAlive(pitID, pointInTimeKeepAliveInterval), 1285 }, 1286 ).Return(searchResult, nil) 1287 1288 token := &visibilityPageToken{ 1289 SearchAfter: searchAfter, 1290 PointInTimeID: pitID, 1291 } 1292 tokenBytes, err := s.visibilityStore.serializePageToken(token) 1293 s.NoError(err) 1294 1295 result, err := s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1296 s.NoError(err) 1297 s.Equal(tokenBytes, result.NextPageToken) 1298 1299 // test last page 1300 request.NextPageToken = tokenBytes 1301 searchResult = &elastic.SearchResult{ 1302 Hits: &elastic.SearchHits{ 1303 Hits: []*elastic.SearchHit{}, 1304 }, 1305 PitId: pitID, 1306 } 1307 s.mockESClient.EXPECT().ClosePointInTime(gomock.Any(), pitID).Return(true, nil) 1308 s.mockESClient.EXPECT().Search( 1309 gomock.Any(), 1310 &client.SearchParameters{ 1311 Index: testIndex, 1312 Query: elastic.NewBoolQuery(). 1313 Filter( 1314 elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String()), 1315 elastic.NewBoolQuery().Filter( 1316 elastic.NewMatchQuery( 1317 searchattribute.ExecutionStatus, 1318 enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String(), 1319 ), 1320 ), 1321 ). 1322 MustNot(elastic.NewExistsQuery(searchattribute.TemporalNamespaceDivision)), 1323 PageSize: 1, 1324 Sorter: docSorter, 1325 SearchAfter: token.SearchAfter, 1326 PointInTime: elastic.NewPointInTimeWithKeepAlive(pitID, pointInTimeKeepAliveInterval), 1327 }, 1328 ).Return(searchResult, nil) 1329 result, err = s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1330 s.NoError(err) 1331 s.Nil(result.NextPageToken) 1332 1333 // test unavailable error 1334 s.mockESClient.EXPECT().Search(gomock.Any(), gomock.Any()).Return(nil, errTestESSearch) 1335 _, err = s.visibilityStore.ScanWorkflowExecutions(context.Background(), request) 1336 s.Error(err) 1337 _, ok = err.(*serviceerror.Unavailable) 1338 s.True(ok) 1339 s.Contains(err.Error(), "ScanWorkflowExecutions failed") 1340 } 1341 1342 func (s *ESVisibilitySuite) TestCountWorkflowExecutions() { 1343 s.mockESClient.EXPECT().Count(gomock.Any(), testIndex, gomock.Any()).DoAndReturn( 1344 func(ctx context.Context, index string, query elastic.Query) (int64, error) { 1345 s.Equal( 1346 elastic.NewBoolQuery().Filter( 1347 elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String()), 1348 elastic.NewBoolQuery().Filter(elastic.NewMatchQuery("ExecutionStatus", "Terminated")), 1349 ).MustNot(namespaceDivisionExists), 1350 query, 1351 ) 1352 return int64(1), nil 1353 }) 1354 1355 request := &manager.CountWorkflowExecutionsRequest{ 1356 NamespaceID: testNamespaceID, 1357 Namespace: testNamespace, 1358 Query: `ExecutionStatus = "Terminated"`, 1359 } 1360 resp, err := s.visibilityStore.CountWorkflowExecutions(context.Background(), request) 1361 s.NoError(err) 1362 s.Equal(int64(1), resp.Count) 1363 1364 // test unavailable error 1365 s.mockESClient.EXPECT().Count(gomock.Any(), testIndex, gomock.Any()).DoAndReturn( 1366 func(ctx context.Context, index string, query elastic.Query) (int64, error) { 1367 s.Equal( 1368 elastic.NewBoolQuery().Filter( 1369 elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String()), 1370 elastic.NewBoolQuery().Filter(elastic.NewMatchQuery("ExecutionStatus", "Terminated")), 1371 ).MustNot(namespaceDivisionExists), 1372 query, 1373 ) 1374 return int64(0), errTestESSearch 1375 }) 1376 1377 _, err = s.visibilityStore.CountWorkflowExecutions(context.Background(), request) 1378 s.Error(err) 1379 _, ok := err.(*serviceerror.Unavailable) 1380 s.True(ok) 1381 s.Contains(err.Error(), "CountWorkflowExecutions failed") 1382 1383 // test bad request 1384 request.Query = `invalid query` 1385 _, err = s.visibilityStore.CountWorkflowExecutions(context.Background(), request) 1386 s.Error(err) 1387 _, ok = err.(*serviceerror.InvalidArgument) 1388 s.True(ok) 1389 s.True(strings.HasPrefix(err.Error(), "invalid query"), err.Error()) 1390 } 1391 1392 func (s *ESVisibilitySuite) TestCountWorkflowExecutions_GroupBy() { 1393 request := &manager.CountWorkflowExecutionsRequest{ 1394 NamespaceID: testNamespaceID, 1395 Namespace: testNamespace, 1396 Query: "GROUP BY ExecutionStatus", 1397 } 1398 s.mockESClient.EXPECT(). 1399 CountGroupBy( 1400 gomock.Any(), 1401 testIndex, 1402 elastic.NewBoolQuery(). 1403 Filter(elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String())). 1404 MustNot(namespaceDivisionExists), 1405 searchattribute.ExecutionStatus, 1406 elastic.NewTermsAggregation().Field(searchattribute.ExecutionStatus), 1407 ). 1408 Return( 1409 &elastic.SearchResult{ 1410 Aggregations: map[string]json.RawMessage{ 1411 searchattribute.ExecutionStatus: json.RawMessage( 1412 `{"buckets":[{"key":"Completed","doc_count":100},{"key":"Running","doc_count":10}]}`, 1413 ), 1414 }, 1415 }, 1416 nil, 1417 ) 1418 resp, err := s.visibilityStore.CountWorkflowExecutions(context.Background(), request) 1419 s.NoError(err) 1420 payload1, _ := searchattribute.EncodeValue( 1421 enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, 1422 enumspb.INDEXED_VALUE_TYPE_KEYWORD, 1423 ) 1424 payload2, _ := searchattribute.EncodeValue( 1425 enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, 1426 enumspb.INDEXED_VALUE_TYPE_KEYWORD, 1427 ) 1428 s.True(temporalproto.DeepEqual( 1429 &manager.CountWorkflowExecutionsResponse{ 1430 Count: 110, 1431 Groups: []*workflowservice.CountWorkflowExecutionsResponse_AggregationGroup{ 1432 { 1433 GroupValues: []*commonpb.Payload{payload1}, 1434 Count: 100, 1435 }, 1436 { 1437 GroupValues: []*commonpb.Payload{payload2}, 1438 Count: 10, 1439 }, 1440 }, 1441 }, 1442 resp), 1443 ) 1444 1445 // test only allowed to group by a single field 1446 request.Query = "GROUP BY ExecutionStatus, WorkflowType" 1447 resp, err = s.visibilityStore.CountWorkflowExecutions(context.Background(), request) 1448 s.Error(err) 1449 s.Contains(err.Error(), "'group by' clause supports only a single field") 1450 s.Nil(resp) 1451 1452 // test only allowed to group by ExecutionStatus 1453 request.Query = "GROUP BY WorkflowType" 1454 resp, err = s.visibilityStore.CountWorkflowExecutions(context.Background(), request) 1455 s.Error(err) 1456 s.Contains(err.Error(), "'group by' clause is only supported for ExecutionStatus search attribute") 1457 s.Nil(resp) 1458 } 1459 1460 func (s *ESVisibilitySuite) TestCountGroupByWorkflowExecutions() { 1461 statusCompletedPayload, _ := searchattribute.EncodeValue( 1462 enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, 1463 enumspb.INDEXED_VALUE_TYPE_KEYWORD, 1464 ) 1465 statusRunningPayload, _ := searchattribute.EncodeValue( 1466 enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, 1467 enumspb.INDEXED_VALUE_TYPE_KEYWORD, 1468 ) 1469 wfType1Payload, _ := searchattribute.EncodeValue("wf-type-1", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1470 wfType2Payload, _ := searchattribute.EncodeValue("wf-type-2", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1471 wfId1Payload, _ := searchattribute.EncodeValue("wf-id-1", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1472 wfId2Payload, _ := searchattribute.EncodeValue("wf-id-2", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1473 wfId3Payload, _ := searchattribute.EncodeValue("wf-id-3", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1474 wfId4Payload, _ := searchattribute.EncodeValue("wf-id-4", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1475 wfId5Payload, _ := searchattribute.EncodeValue("wf-id-5", enumspb.INDEXED_VALUE_TYPE_KEYWORD) 1476 1477 testCases := []struct { 1478 name string 1479 groupBy []string 1480 aggName string 1481 agg elastic.Aggregation 1482 mockResponse *elastic.SearchResult 1483 response *manager.CountWorkflowExecutionsResponse 1484 }{ 1485 { 1486 name: "group by one field", 1487 groupBy: []string{searchattribute.ExecutionStatus}, 1488 aggName: searchattribute.ExecutionStatus, 1489 agg: elastic.NewTermsAggregation().Field(searchattribute.ExecutionStatus), 1490 mockResponse: &elastic.SearchResult{ 1491 Aggregations: map[string]json.RawMessage{ 1492 searchattribute.ExecutionStatus: json.RawMessage( 1493 `{ 1494 "buckets":[ 1495 { 1496 "key": "Completed", 1497 "doc_count": 100 1498 }, 1499 { 1500 "key": "Running", 1501 "doc_count": 10 1502 } 1503 ] 1504 }`, 1505 ), 1506 }, 1507 }, 1508 response: &manager.CountWorkflowExecutionsResponse{ 1509 Count: 110, 1510 Groups: []*workflowservice.CountWorkflowExecutionsResponse_AggregationGroup{ 1511 { 1512 GroupValues: []*commonpb.Payload{statusCompletedPayload}, 1513 Count: 100, 1514 }, 1515 { 1516 GroupValues: []*commonpb.Payload{statusRunningPayload}, 1517 Count: 10, 1518 }, 1519 }, 1520 }, 1521 }, 1522 1523 { 1524 name: "group by two fields", 1525 groupBy: []string{searchattribute.ExecutionStatus, searchattribute.WorkflowType}, 1526 aggName: searchattribute.ExecutionStatus, 1527 agg: elastic.NewTermsAggregation().Field(searchattribute.ExecutionStatus).SubAggregation( 1528 searchattribute.WorkflowType, 1529 elastic.NewTermsAggregation().Field(searchattribute.WorkflowType), 1530 ), 1531 mockResponse: &elastic.SearchResult{ 1532 Aggregations: map[string]json.RawMessage{ 1533 searchattribute.ExecutionStatus: json.RawMessage( 1534 `{ 1535 "buckets":[ 1536 { 1537 "key": "Completed", 1538 "doc_count": 100, 1539 "WorkflowType": { 1540 "buckets": [ 1541 { 1542 "key": "wf-type-1", 1543 "doc_count": 75 1544 }, 1545 { 1546 "key": "wf-type-2", 1547 "doc_count": 25 1548 } 1549 ] 1550 } 1551 }, 1552 { 1553 "key": "Running", 1554 "doc_count": 10, 1555 "WorkflowType": { 1556 "buckets": [ 1557 { 1558 "key": "wf-type-1", 1559 "doc_count": 7 1560 }, 1561 { 1562 "key": "wf-type-2", 1563 "doc_count": 3 1564 } 1565 ] 1566 } 1567 } 1568 ] 1569 }`, 1570 ), 1571 }, 1572 }, 1573 response: &manager.CountWorkflowExecutionsResponse{ 1574 Count: 110, 1575 Groups: []*workflowservice.CountWorkflowExecutionsResponse_AggregationGroup{ 1576 { 1577 GroupValues: []*commonpb.Payload{statusCompletedPayload, wfType1Payload}, 1578 Count: 75, 1579 }, 1580 { 1581 GroupValues: []*commonpb.Payload{statusCompletedPayload, wfType2Payload}, 1582 Count: 25, 1583 }, 1584 { 1585 GroupValues: []*commonpb.Payload{statusRunningPayload, wfType1Payload}, 1586 Count: 7, 1587 }, 1588 { 1589 GroupValues: []*commonpb.Payload{statusRunningPayload, wfType2Payload}, 1590 Count: 3, 1591 }, 1592 }, 1593 }, 1594 }, 1595 1596 { 1597 name: "group by three fields", 1598 groupBy: []string{ 1599 searchattribute.ExecutionStatus, 1600 searchattribute.WorkflowType, 1601 searchattribute.WorkflowID, 1602 }, 1603 aggName: searchattribute.ExecutionStatus, 1604 agg: elastic.NewTermsAggregation().Field(searchattribute.ExecutionStatus).SubAggregation( 1605 searchattribute.WorkflowType, 1606 elastic.NewTermsAggregation().Field(searchattribute.WorkflowType).SubAggregation( 1607 searchattribute.WorkflowID, 1608 elastic.NewTermsAggregation().Field(searchattribute.WorkflowID), 1609 ), 1610 ), 1611 mockResponse: &elastic.SearchResult{ 1612 Aggregations: map[string]json.RawMessage{ 1613 searchattribute.ExecutionStatus: json.RawMessage( 1614 `{ 1615 "buckets":[ 1616 { 1617 "key": "Completed", 1618 "doc_count": 100, 1619 "WorkflowType": { 1620 "buckets": [ 1621 { 1622 "key": "wf-type-1", 1623 "doc_count": 75, 1624 "WorkflowId": { 1625 "buckets": [ 1626 { 1627 "key": "wf-id-1", 1628 "doc_count": 75 1629 } 1630 ] 1631 } 1632 }, 1633 { 1634 "key": "wf-type-2", 1635 "doc_count": 25, 1636 "WorkflowId": { 1637 "buckets": [ 1638 { 1639 "key": "wf-id-2", 1640 "doc_count": 20 1641 }, 1642 { 1643 "key": "wf-id-3", 1644 "doc_count": 5 1645 } 1646 ] 1647 } 1648 } 1649 ] 1650 } 1651 }, 1652 { 1653 "key": "Running", 1654 "doc_count": 10, 1655 "WorkflowType": { 1656 "buckets": [ 1657 { 1658 "key": "wf-type-1", 1659 "doc_count": 7, 1660 "WorkflowId": { 1661 "buckets": [ 1662 { 1663 "key": "wf-id-4", 1664 "doc_count": 7 1665 } 1666 ] 1667 } 1668 }, 1669 { 1670 "key": "wf-type-2", 1671 "doc_count": 3, 1672 "WorkflowId": { 1673 "buckets": [ 1674 { 1675 "key": "wf-id-5", 1676 "doc_count": 3 1677 } 1678 ] 1679 } 1680 } 1681 ] 1682 } 1683 } 1684 ] 1685 }`, 1686 ), 1687 }, 1688 }, 1689 response: &manager.CountWorkflowExecutionsResponse{ 1690 Count: 110, 1691 Groups: []*workflowservice.CountWorkflowExecutionsResponse_AggregationGroup{ 1692 { 1693 GroupValues: []*commonpb.Payload{statusCompletedPayload, wfType1Payload, wfId1Payload}, 1694 Count: 75, 1695 }, 1696 { 1697 GroupValues: []*commonpb.Payload{statusCompletedPayload, wfType2Payload, wfId2Payload}, 1698 Count: 20, 1699 }, 1700 { 1701 GroupValues: []*commonpb.Payload{statusCompletedPayload, wfType2Payload, wfId3Payload}, 1702 Count: 5, 1703 }, 1704 { 1705 GroupValues: []*commonpb.Payload{statusRunningPayload, wfType1Payload, wfId4Payload}, 1706 Count: 7, 1707 }, 1708 { 1709 GroupValues: []*commonpb.Payload{statusRunningPayload, wfType2Payload, wfId5Payload}, 1710 Count: 3, 1711 }, 1712 }, 1713 }, 1714 }, 1715 } 1716 1717 for _, tc := range testCases { 1718 s.T().Run(tc.name, func(t *testing.T) { 1719 searchParams := &query.QueryParams{ 1720 Query: elastic.NewBoolQuery(). 1721 Filter(elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String())). 1722 MustNot(namespaceDivisionExists), 1723 GroupBy: tc.groupBy, 1724 } 1725 s.mockESClient.EXPECT(). 1726 CountGroupBy( 1727 gomock.Any(), 1728 testIndex, 1729 elastic.NewBoolQuery(). 1730 Filter(elastic.NewTermQuery(searchattribute.NamespaceID, testNamespaceID.String())). 1731 MustNot(namespaceDivisionExists), 1732 tc.aggName, 1733 tc.agg, 1734 ). 1735 Return(tc.mockResponse, nil) 1736 resp, err := s.visibilityStore.countGroupByWorkflowExecutions(context.Background(), searchParams) 1737 s.NoError(err) 1738 s.True(temporalproto.DeepEqual(tc.response, resp)) 1739 }) 1740 } 1741 } 1742 1743 func (s *ESVisibilitySuite) TestGetWorkflowExecution() { 1744 now := timestamppb.New(time.Now()) 1745 s.mockESClient.EXPECT().Get(gomock.Any(), testIndex, gomock.Any()).DoAndReturn( 1746 func(ctx context.Context, index string, docID string) (*elastic.GetResult, error) { 1747 s.Equal(testIndex, index) 1748 s.Equal(testWorkflowID+delimiter+testRunID, docID) 1749 data := map[string]interface{}{ 1750 "ExecutionStatus": "Running", 1751 "NamespaceId": testNamespaceID.String(), 1752 "StateTransitionCount": 22, 1753 "VisibilityTaskKey": "7-619", 1754 "RunId": testRunID, 1755 "StartTime": "2021-06-11T15:04:07.980-07:00", 1756 "WorkflowId": testWorkflowID, 1757 "WorkflowType": "basic.stressWorkflowExecute", 1758 } 1759 source, _ := json.Marshal(data) 1760 return &elastic.GetResult{Found: true, Source: source}, nil 1761 }) 1762 1763 request := &manager.GetWorkflowExecutionRequest{ 1764 NamespaceID: testNamespaceID, 1765 Namespace: testNamespace, 1766 WorkflowID: testWorkflowID, 1767 RunID: testRunID, 1768 CloseTime: now.AsTime(), 1769 } 1770 _, err := s.visibilityStore.GetWorkflowExecution(context.Background(), request) 1771 s.NoError(err) 1772 1773 // test unavailable error 1774 s.mockESClient.EXPECT().Get(gomock.Any(), testIndex, gomock.Any()).DoAndReturn( 1775 func(ctx context.Context, index string, docID string) (*elastic.GetResult, error) { 1776 s.Equal(testIndex, index) 1777 s.Equal(testWorkflowID+delimiter+testRunID, docID) 1778 return nil, errTestESSearch 1779 }) 1780 1781 _, err = s.visibilityStore.GetWorkflowExecution(context.Background(), request) 1782 s.Error(err) 1783 _, ok := err.(*serviceerror.Unavailable) 1784 s.True(ok) 1785 s.Contains(err.Error(), "GetWorkflowExecution failed") 1786 } 1787 1788 func (s *ESVisibilitySuite) Test_detailedErrorMessage() { 1789 err := errors.New("test message") 1790 s.Equal("test message", detailedErrorMessage(err)) 1791 1792 err = &elastic.Error{ 1793 Status: 500, 1794 } 1795 s.Equal("elastic: Error 500 (Internal Server Error)", detailedErrorMessage(err)) 1796 1797 err = &elastic.Error{ 1798 Status: 500, 1799 Details: &elastic.ErrorDetails{ 1800 Type: "some type", 1801 Reason: "some reason", 1802 }, 1803 } 1804 s.Equal("elastic: Error 500 (Internal Server Error): some reason [type=some type]", detailedErrorMessage(err)) 1805 1806 err = &elastic.Error{ 1807 Status: 500, 1808 Details: &elastic.ErrorDetails{ 1809 Type: "some type", 1810 Reason: "some reason", 1811 RootCause: []*elastic.ErrorDetails{ 1812 { 1813 Type: "some type", 1814 Reason: "some reason", 1815 }, 1816 }, 1817 }, 1818 } 1819 s.Equal("elastic: Error 500 (Internal Server Error): some reason [type=some type]", detailedErrorMessage(err)) 1820 1821 err = &elastic.Error{ 1822 Status: 500, 1823 Details: &elastic.ErrorDetails{ 1824 Type: "some type", 1825 Reason: "some reason", 1826 RootCause: []*elastic.ErrorDetails{ 1827 { 1828 Type: "some other type1", 1829 Reason: "some other reason1", 1830 }, 1831 { 1832 Type: "some other type2", 1833 Reason: "some other reason2", 1834 }, 1835 }, 1836 }, 1837 } 1838 s.Equal("elastic: Error 500 (Internal Server Error): some reason [type=some type], root causes: some other reason1 [type=some other type1], some other reason2 [type=some other type2]", detailedErrorMessage(err)) 1839 } 1840 1841 func (s *ESVisibilitySuite) TestProcessPageToken() { 1842 closeTime := time.Now().UTC() 1843 startTime := closeTime.Add(-1 * time.Minute) 1844 baseQuery := elastic.NewBoolQuery(). 1845 Filter(elastic.NewTermQuery(searchattribute.NamespaceID, testNamespace.String())) 1846 1847 testCases := []struct { 1848 name string 1849 manualPagination bool 1850 sorter []elastic.Sorter 1851 pageToken *visibilityPageToken 1852 resSearchAfter []any 1853 resQuery elastic.Query 1854 resError error 1855 }{ 1856 { 1857 name: "nil page token", 1858 manualPagination: false, 1859 sorter: docSorter, 1860 pageToken: nil, 1861 resSearchAfter: nil, 1862 resQuery: baseQuery, 1863 resError: nil, 1864 }, 1865 { 1866 name: "empty page token", 1867 manualPagination: false, 1868 sorter: docSorter, 1869 pageToken: &visibilityPageToken{SearchAfter: []any{}}, 1870 resSearchAfter: nil, 1871 resQuery: baseQuery, 1872 resError: nil, 1873 }, 1874 { 1875 name: "page token doesn't match sorter size", 1876 manualPagination: false, 1877 sorter: docSorter, 1878 pageToken: &visibilityPageToken{SearchAfter: []any{"foo", "bar"}}, 1879 resSearchAfter: nil, 1880 resQuery: baseQuery, 1881 resError: serviceerror.NewInvalidArgument("Invalid page token for given sort fields: expected 1 fields, got 2"), 1882 }, 1883 { 1884 name: "not using default sorter", 1885 manualPagination: false, 1886 sorter: docSorter, 1887 pageToken: &visibilityPageToken{SearchAfter: []any{123}}, 1888 resSearchAfter: []any{123}, 1889 resQuery: baseQuery, 1890 resError: nil, 1891 }, 1892 { 1893 name: "default sorter without manual pagination", 1894 manualPagination: false, 1895 sorter: defaultSorter, 1896 pageToken: &visibilityPageToken{ 1897 SearchAfter: []any{ 1898 json.Number(fmt.Sprintf("%d", closeTime.UnixNano())), 1899 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 1900 }, 1901 }, 1902 resSearchAfter: []any{ 1903 json.Number(fmt.Sprintf("%d", closeTime.UnixNano())), 1904 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 1905 }, 1906 resQuery: baseQuery, 1907 resError: nil, 1908 }, 1909 { 1910 name: "default sorter with manual pagination", 1911 manualPagination: true, 1912 sorter: defaultSorter, 1913 pageToken: &visibilityPageToken{ 1914 SearchAfter: []any{ 1915 json.Number(fmt.Sprintf("%d", closeTime.UnixNano())), 1916 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 1917 }, 1918 }, 1919 resSearchAfter: nil, 1920 resQuery: baseQuery.MinimumNumberShouldMatch(1).Should( 1921 elastic.NewBoolQuery().Filter( 1922 elastic.NewRangeQuery(searchattribute.CloseTime).Lt(closeTime.Format(time.RFC3339Nano)), 1923 ), 1924 elastic.NewBoolQuery().Filter( 1925 elastic.NewTermQuery(searchattribute.CloseTime, closeTime.Format(time.RFC3339Nano)), 1926 elastic.NewRangeQuery(searchattribute.StartTime).Lt(startTime.Format(time.RFC3339Nano)), 1927 ), 1928 ), 1929 resError: nil, 1930 }, 1931 } 1932 1933 for _, tc := range testCases { 1934 s.T().Run(tc.name, func(t *testing.T) { 1935 visibilityStore := NewVisibilityStore( 1936 s.mockESClient, 1937 testIndex, 1938 searchattribute.NewTestProvider(), 1939 searchattribute.NewTestMapperProvider(nil), 1940 s.mockProcessor, 1941 dynamicconfig.GetDurationPropertyFn(1*time.Minute*debug.TimeoutMultiplier), 1942 dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), 1943 dynamicconfig.GetBoolPropertyFnFilteredByNamespace(tc.manualPagination), 1944 s.mockMetricsHandler, 1945 ) 1946 params := &client.SearchParameters{ 1947 Index: testIndex, 1948 Query: baseQuery, 1949 Sorter: tc.sorter, 1950 } 1951 err := visibilityStore.processPageToken(params, tc.pageToken, testNamespace) 1952 s.Equal(tc.resSearchAfter, params.SearchAfter) 1953 s.Equal(tc.resQuery, params.Query) 1954 s.Equal(tc.resError, err) 1955 }) 1956 } 1957 } 1958 1959 func (s *ESVisibilitySuite) Test_buildPaginationQuery() { 1960 startTime := time.Now().UTC() 1961 closeTime := startTime.Add(1 * time.Minute) 1962 datetimeNull := json.Number(fmt.Sprintf("%d", math.MaxInt64)) 1963 1964 testCases := []struct { 1965 name string 1966 sorterFields []fieldSort 1967 searchAfter []any 1968 res []elastic.Query 1969 err error 1970 }{ 1971 { 1972 name: "one field", 1973 sorterFields: []fieldSort{{searchattribute.StartTime, true, true}}, 1974 searchAfter: []any{json.Number(fmt.Sprintf("%d", startTime.UnixNano()))}, 1975 res: []elastic.Query{ 1976 elastic.NewBoolQuery().Filter( 1977 elastic.NewRangeQuery(searchattribute.StartTime).Lt(startTime.Format(time.RFC3339Nano)), 1978 ), 1979 }, 1980 err: nil, 1981 }, 1982 { 1983 name: "two fields one null", 1984 sorterFields: []fieldSort{ 1985 {searchattribute.CloseTime, true, true}, 1986 {searchattribute.StartTime, true, true}, 1987 }, 1988 searchAfter: []any{ 1989 datetimeNull, 1990 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 1991 }, 1992 res: []elastic.Query{ 1993 elastic.NewBoolQuery().Filter(elastic.NewExistsQuery(searchattribute.CloseTime)), 1994 elastic.NewBoolQuery(). 1995 MustNot(elastic.NewExistsQuery(searchattribute.CloseTime)). 1996 Filter( 1997 elastic.NewRangeQuery(searchattribute.StartTime).Lt(startTime.Format(time.RFC3339Nano)), 1998 ), 1999 }, 2000 err: nil, 2001 }, 2002 { 2003 name: "two fields no null", 2004 sorterFields: []fieldSort{ 2005 {searchattribute.CloseTime, true, true}, 2006 {searchattribute.StartTime, true, true}, 2007 }, 2008 searchAfter: []any{ 2009 json.Number(fmt.Sprintf("%d", closeTime.UnixNano())), 2010 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 2011 }, 2012 res: []elastic.Query{ 2013 elastic.NewBoolQuery().Filter( 2014 elastic.NewRangeQuery(searchattribute.CloseTime).Lt(closeTime.Format(time.RFC3339Nano)), 2015 ), 2016 elastic.NewBoolQuery(). 2017 Filter( 2018 elastic.NewTermQuery(searchattribute.CloseTime, closeTime.Format(time.RFC3339Nano)), 2019 elastic.NewRangeQuery(searchattribute.StartTime).Lt(startTime.Format(time.RFC3339Nano)), 2020 ), 2021 }, 2022 err: nil, 2023 }, 2024 { 2025 name: "three fields", 2026 sorterFields: []fieldSort{ 2027 {searchattribute.CloseTime, true, true}, 2028 {searchattribute.StartTime, true, true}, 2029 {searchattribute.RunID, false, true}, 2030 }, 2031 searchAfter: []any{ 2032 json.Number(fmt.Sprintf("%d", closeTime.UnixNano())), 2033 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 2034 "random-run-id", 2035 }, 2036 res: []elastic.Query{ 2037 elastic.NewBoolQuery().Filter( 2038 elastic.NewRangeQuery(searchattribute.CloseTime).Lt(closeTime.Format(time.RFC3339Nano)), 2039 ), 2040 elastic.NewBoolQuery(). 2041 Filter( 2042 elastic.NewTermQuery(searchattribute.CloseTime, closeTime.Format(time.RFC3339Nano)), 2043 elastic.NewRangeQuery(searchattribute.StartTime).Lt(startTime.Format(time.RFC3339Nano)), 2044 ), 2045 elastic.NewBoolQuery(). 2046 Filter( 2047 elastic.NewTermQuery(searchattribute.CloseTime, closeTime.Format(time.RFC3339Nano)), 2048 elastic.NewTermQuery(searchattribute.StartTime, startTime.Format(time.RFC3339Nano)), 2049 elastic.NewRangeQuery(searchattribute.RunID).Gt("random-run-id"), 2050 ), 2051 }, 2052 err: nil, 2053 }, 2054 { 2055 name: "invalid token: wrong size", 2056 sorterFields: []fieldSort{ 2057 {searchattribute.CloseTime, true, true}, 2058 {searchattribute.StartTime, true, true}, 2059 {searchattribute.RunID, false, true}, 2060 }, 2061 searchAfter: []any{ 2062 json.Number(fmt.Sprintf("%d", closeTime.UnixNano())), 2063 json.Number(fmt.Sprintf("%d", startTime.UnixNano())), 2064 }, 2065 res: nil, 2066 err: serviceerror.NewInvalidArgument("Invalid page token for given sort fields: expected 3 fields, got 2"), 2067 }, 2068 { 2069 name: "invalid token: last value null", 2070 sorterFields: []fieldSort{ 2071 {searchattribute.CloseTime, true, true}, 2072 }, 2073 searchAfter: []any{datetimeNull}, 2074 res: nil, 2075 err: serviceerror.NewInternal("Last field of sorter cannot be a nullable field: \"CloseTime\" has null values"), 2076 }, 2077 } 2078 2079 for _, tc := range testCases { 2080 s.T().Run(tc.name, func(t *testing.T) { 2081 res, err := buildPaginationQuery(tc.sorterFields, tc.searchAfter, searchattribute.TestNameTypeMap) 2082 s.Equal(tc.err, err) 2083 s.Equal(tc.res, res) 2084 }) 2085 } 2086 } 2087 2088 func (s *ESVisibilitySuite) Test_parsePageTokenValue() { 2089 testCases := []struct { 2090 name string 2091 value any 2092 tp enumspb.IndexedValueType 2093 res any 2094 err error 2095 }{ 2096 { 2097 name: "IntField", 2098 value: 123, 2099 tp: enumspb.INDEXED_VALUE_TYPE_INT, 2100 res: int64(123), 2101 err: nil, 2102 }, 2103 { 2104 name: "NullMaxIntField", 2105 value: math.MaxInt64, 2106 tp: enumspb.INDEXED_VALUE_TYPE_INT, 2107 res: nil, 2108 err: nil, 2109 }, 2110 { 2111 name: "NullMinIntField", 2112 value: math.MinInt64, 2113 tp: enumspb.INDEXED_VALUE_TYPE_INT, 2114 res: nil, 2115 err: nil, 2116 }, 2117 { 2118 name: "BoolFieldTrue", 2119 value: 1, 2120 tp: enumspb.INDEXED_VALUE_TYPE_BOOL, 2121 res: true, 2122 err: nil, 2123 }, 2124 { 2125 name: "BoolFieldFalse", 2126 value: 0, 2127 tp: enumspb.INDEXED_VALUE_TYPE_BOOL, 2128 res: false, 2129 err: nil, 2130 }, 2131 { 2132 name: "NullMaxBoolField", 2133 value: math.MaxInt64, 2134 tp: enumspb.INDEXED_VALUE_TYPE_BOOL, 2135 res: nil, 2136 err: nil, 2137 }, 2138 { 2139 name: "NullMinBoolField", 2140 value: math.MinInt64, 2141 tp: enumspb.INDEXED_VALUE_TYPE_BOOL, 2142 res: nil, 2143 err: nil, 2144 }, 2145 { 2146 name: "DatetimeField", 2147 value: 1683221689123456789, 2148 tp: enumspb.INDEXED_VALUE_TYPE_DATETIME, 2149 res: "2023-05-04T17:34:49.123456789Z", 2150 err: nil, 2151 }, 2152 { 2153 name: "NullMaxDatetimeField", 2154 value: math.MaxInt64, 2155 tp: enumspb.INDEXED_VALUE_TYPE_DATETIME, 2156 res: nil, 2157 err: nil, 2158 }, 2159 { 2160 name: "NullMinDatetimeField", 2161 value: math.MinInt64, 2162 tp: enumspb.INDEXED_VALUE_TYPE_DATETIME, 2163 res: nil, 2164 err: nil, 2165 }, 2166 { 2167 name: "DoubleField", 2168 value: 3.14, 2169 tp: enumspb.INDEXED_VALUE_TYPE_DOUBLE, 2170 res: float64(3.14), 2171 err: nil, 2172 }, 2173 { 2174 name: "NullMaxDoubleField", 2175 value: "Infinity", 2176 tp: enumspb.INDEXED_VALUE_TYPE_DOUBLE, 2177 res: nil, 2178 err: nil, 2179 }, 2180 { 2181 name: "NullMinDoubleField", 2182 value: "-Infinity", 2183 tp: enumspb.INDEXED_VALUE_TYPE_DOUBLE, 2184 res: nil, 2185 err: nil, 2186 }, 2187 { 2188 name: "KeywordField", 2189 value: "foo", 2190 tp: enumspb.INDEXED_VALUE_TYPE_KEYWORD, 2191 res: "foo", 2192 err: nil, 2193 }, 2194 { 2195 name: "NullKeywordField", 2196 value: nil, 2197 tp: enumspb.INDEXED_VALUE_TYPE_KEYWORD, 2198 res: nil, 2199 err: nil, 2200 }, 2201 { 2202 name: "IntFieldError", 2203 value: "123", 2204 tp: enumspb.INDEXED_VALUE_TYPE_INT, 2205 res: nil, 2206 err: serviceerror.NewInvalidArgument("Invalid page token: expected interger type, got \"123\""), 2207 }, 2208 { 2209 name: "DoubleFieldError", 2210 value: "foo", 2211 tp: enumspb.INDEXED_VALUE_TYPE_DOUBLE, 2212 res: nil, 2213 err: serviceerror.NewInvalidArgument("Invalid page token: expected float type, got \"foo\""), 2214 }, 2215 { 2216 name: "KeywordFieldError", 2217 value: 123, 2218 tp: enumspb.INDEXED_VALUE_TYPE_KEYWORD, 2219 res: nil, 2220 err: serviceerror.NewInvalidArgument("Invalid page token: expected string type, got 123"), 2221 }, 2222 { 2223 name: "TextFieldError", 2224 value: "foo", 2225 tp: enumspb.INDEXED_VALUE_TYPE_TEXT, 2226 res: nil, 2227 err: serviceerror.NewInvalidArgument("Invalid field type in sorter: cannot order by \"TextFieldError\""), 2228 }, 2229 } 2230 2231 pageToken := &visibilityPageToken{} 2232 for _, tc := range testCases { 2233 pageToken.SearchAfter = append(pageToken.SearchAfter, tc.value) 2234 } 2235 jsonToken, _ := json.Marshal(pageToken) 2236 pageToken, err := s.visibilityStore.deserializePageToken(jsonToken) 2237 s.NoError(err) 2238 s.Equal(len(testCases), len(pageToken.SearchAfter)) 2239 for i, tc := range testCases { 2240 s.T().Run(tc.name, func(t *testing.T) { 2241 res, err := parsePageTokenValue(tc.name, pageToken.SearchAfter[i], tc.tp) 2242 s.Equal(tc.err, err) 2243 s.Equal(tc.res, res) 2244 }) 2245 } 2246 }