go.temporal.io/server@v1.23.0/common/persistence/visibility/store/elasticsearch/visibility_store.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 "bytes" 29 "context" 30 "encoding/base64" 31 "encoding/json" 32 "errors" 33 "fmt" 34 "io" 35 "math" 36 "strconv" 37 "strings" 38 "time" 39 40 "github.com/olivere/elastic/v7" 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 46 "go.temporal.io/server/common/dynamicconfig" 47 "go.temporal.io/server/common/metrics" 48 "go.temporal.io/server/common/namespace" 49 "go.temporal.io/server/common/persistence" 50 "go.temporal.io/server/common/persistence/visibility/manager" 51 "go.temporal.io/server/common/persistence/visibility/store" 52 "go.temporal.io/server/common/persistence/visibility/store/elasticsearch/client" 53 "go.temporal.io/server/common/persistence/visibility/store/query" 54 "go.temporal.io/server/common/searchattribute" 55 ) 56 57 const ( 58 PersistenceName = "elasticsearch" 59 60 delimiter = "~" 61 scrollKeepAliveInterval = "1m" 62 pointInTimeKeepAliveInterval = "1m" 63 ) 64 65 type ( 66 visibilityStore struct { 67 esClient client.Client 68 index string 69 searchAttributesProvider searchattribute.Provider 70 searchAttributesMapperProvider searchattribute.MapperProvider 71 processor Processor 72 processorAckTimeout dynamicconfig.DurationPropertyFn 73 disableOrderByClause dynamicconfig.BoolPropertyFnWithNamespaceFilter 74 enableManualPagination dynamicconfig.BoolPropertyFnWithNamespaceFilter 75 metricsHandler metrics.Handler 76 } 77 78 visibilityPageToken struct { 79 SearchAfter []interface{} 80 81 // For ScanWorkflowExecutions API. 82 // For ES<7.10.0 and "oss" flavor. 83 ScrollID string 84 // For ES>=7.10.0 and "default" flavor. 85 PointInTimeID string 86 } 87 88 fieldSort struct { 89 name string 90 desc bool 91 missing_first bool 92 } 93 ) 94 95 var _ store.VisibilityStore = (*visibilityStore)(nil) 96 97 var ( 98 errUnexpectedJSONFieldType = errors.New("unexpected JSON field type") 99 100 minTime = time.Unix(0, 0).UTC() 101 maxTime = time.Unix(0, math.MaxInt64).UTC() 102 maxStringLength = 32766 103 104 // Default sorter uses the sorting order defined in the index template. 105 // It is indirectly built so buildPaginationQuery can have access to 106 // the fields names to build the page query from the token. 107 defaultSorterFields = []fieldSort{ 108 {searchattribute.CloseTime, true, true}, 109 {searchattribute.StartTime, true, true}, 110 } 111 112 defaultSorter = func() []elastic.Sorter { 113 ret := make([]elastic.Sorter, 0, len(defaultSorterFields)) 114 for _, item := range defaultSorterFields { 115 fs := elastic.NewFieldSort(item.name) 116 if item.desc { 117 fs.Desc() 118 } 119 if item.missing_first { 120 fs.Missing("_first") 121 } 122 ret = append(ret, fs) 123 } 124 return ret 125 }() 126 127 docSorter = []elastic.Sorter{ 128 elastic.SortByDoc{}, 129 } 130 ) 131 132 // NewVisibilityStore create a visibility store connecting to ElasticSearch 133 func NewVisibilityStore( 134 esClient client.Client, 135 index string, 136 searchAttributesProvider searchattribute.Provider, 137 searchAttributesMapperProvider searchattribute.MapperProvider, 138 processor Processor, 139 processorAckTimeout dynamicconfig.DurationPropertyFn, 140 disableOrderByClause dynamicconfig.BoolPropertyFnWithNamespaceFilter, 141 enableManualPagination dynamicconfig.BoolPropertyFnWithNamespaceFilter, 142 metricsHandler metrics.Handler, 143 ) *visibilityStore { 144 145 return &visibilityStore{ 146 esClient: esClient, 147 index: index, 148 searchAttributesProvider: searchAttributesProvider, 149 searchAttributesMapperProvider: searchAttributesMapperProvider, 150 processor: processor, 151 processorAckTimeout: processorAckTimeout, 152 disableOrderByClause: disableOrderByClause, 153 enableManualPagination: enableManualPagination, 154 metricsHandler: metricsHandler.WithTags(metrics.OperationTag(metrics.ElasticsearchVisibility)), 155 } 156 } 157 158 func (s *visibilityStore) Close() { 159 // TODO (alex): visibilityStore shouldn't Stop processor. Processor should be stopped where it is created. 160 if s.processor != nil { 161 s.processor.Stop() 162 } 163 } 164 165 func (s *visibilityStore) GetName() string { 166 return PersistenceName 167 } 168 169 func (s *visibilityStore) GetIndexName() string { 170 return s.index 171 } 172 173 func (s *visibilityStore) ValidateCustomSearchAttributes( 174 searchAttributes map[string]any, 175 ) (map[string]any, error) { 176 validatedSearchAttributes := make(map[string]any, len(searchAttributes)) 177 var invalidValueErrs []error 178 for saName, saValue := range searchAttributes { 179 var err error 180 switch value := saValue.(type) { 181 case time.Time: 182 err = validateDatetime(value) 183 case []time.Time: 184 for _, item := range value { 185 if err = validateDatetime(item); err != nil { 186 break 187 } 188 } 189 case string: 190 err = validateString(value) 191 case []string: 192 for _, item := range value { 193 if err = validateString(item); err != nil { 194 break 195 } 196 } 197 } 198 if err != nil { 199 invalidValueErrs = append(invalidValueErrs, err) 200 continue 201 } 202 validatedSearchAttributes[saName] = saValue 203 } 204 var retError error 205 if len(invalidValueErrs) > 0 { 206 retError = store.NewVisibilityStoreInvalidValuesError(invalidValueErrs) 207 } 208 return validatedSearchAttributes, retError 209 } 210 211 func (s *visibilityStore) RecordWorkflowExecutionStarted( 212 ctx context.Context, 213 request *store.InternalRecordWorkflowExecutionStartedRequest, 214 ) error { 215 visibilityTaskKey := getVisibilityTaskKey(request.ShardID, request.TaskID) 216 doc, err := s.generateESDoc(request.InternalVisibilityRequestBase, visibilityTaskKey) 217 if err != nil { 218 return err 219 } 220 221 return s.addBulkIndexRequestAndWait(ctx, request.InternalVisibilityRequestBase, doc, visibilityTaskKey) 222 } 223 224 func (s *visibilityStore) RecordWorkflowExecutionClosed( 225 ctx context.Context, 226 request *store.InternalRecordWorkflowExecutionClosedRequest, 227 ) error { 228 visibilityTaskKey := getVisibilityTaskKey(request.ShardID, request.TaskID) 229 doc, err := s.generateESDoc(request.InternalVisibilityRequestBase, visibilityTaskKey) 230 if err != nil { 231 return err 232 } 233 234 doc[searchattribute.CloseTime] = request.CloseTime 235 doc[searchattribute.ExecutionDuration] = request.ExecutionDuration 236 doc[searchattribute.HistoryLength] = request.HistoryLength 237 doc[searchattribute.StateTransitionCount] = request.StateTransitionCount 238 doc[searchattribute.HistorySizeBytes] = request.HistorySizeBytes 239 240 return s.addBulkIndexRequestAndWait(ctx, request.InternalVisibilityRequestBase, doc, visibilityTaskKey) 241 } 242 243 func (s *visibilityStore) UpsertWorkflowExecution( 244 ctx context.Context, 245 request *store.InternalUpsertWorkflowExecutionRequest, 246 ) error { 247 visibilityTaskKey := getVisibilityTaskKey(request.ShardID, request.TaskID) 248 doc, err := s.generateESDoc(request.InternalVisibilityRequestBase, visibilityTaskKey) 249 if err != nil { 250 return err 251 } 252 253 return s.addBulkIndexRequestAndWait(ctx, request.InternalVisibilityRequestBase, doc, visibilityTaskKey) 254 } 255 256 func (s *visibilityStore) DeleteWorkflowExecution( 257 ctx context.Context, 258 request *manager.VisibilityDeleteWorkflowExecutionRequest, 259 ) error { 260 docID := getDocID(request.WorkflowID, request.RunID) 261 262 bulkDeleteRequest := &client.BulkableRequest{ 263 Index: s.index, 264 ID: docID, 265 Version: request.TaskID, 266 RequestType: client.BulkableRequestTypeDelete, 267 } 268 269 return s.addBulkRequestAndWait(ctx, bulkDeleteRequest, docID) 270 } 271 272 func getDocID(workflowID string, runID string) string { 273 // From Elasticsearch doc: _id is limited to 512 bytes in size and larger values will be rejected. 274 const maxDocIDLength = 512 275 // Generally runID is guid and this should never be the case. 276 if len(runID)+len(delimiter) >= maxDocIDLength { 277 if len(runID) >= maxDocIDLength { 278 return runID[0:maxDocIDLength] 279 } 280 return runID[0 : maxDocIDLength-len(delimiter)] 281 } 282 283 if len(workflowID)+len(runID)+len(delimiter) > maxDocIDLength { 284 workflowID = workflowID[0 : maxDocIDLength-len(runID)-len(delimiter)] 285 } 286 287 return workflowID + delimiter + runID 288 } 289 290 func getVisibilityTaskKey(shardID int32, taskID int64) string { 291 return strconv.FormatInt(int64(shardID), 10) + delimiter + strconv.FormatInt(taskID, 10) 292 } 293 294 func (s *visibilityStore) addBulkIndexRequestAndWait( 295 ctx context.Context, 296 request *store.InternalVisibilityRequestBase, 297 esDoc map[string]interface{}, 298 visibilityTaskKey string, 299 ) error { 300 bulkIndexRequest := &client.BulkableRequest{ 301 Index: s.index, 302 ID: getDocID(request.WorkflowID, request.RunID), 303 Version: request.TaskID, 304 RequestType: client.BulkableRequestTypeIndex, 305 Doc: esDoc, 306 } 307 308 return s.addBulkRequestAndWait(ctx, bulkIndexRequest, visibilityTaskKey) 309 } 310 311 func (s *visibilityStore) addBulkRequestAndWait( 312 _ context.Context, 313 bulkRequest *client.BulkableRequest, 314 visibilityTaskKey string, 315 ) error { 316 s.checkProcessor() 317 318 // Add method is blocking. If bulk processor is busy flushing previous bulk, request will wait here. 319 ackF := s.processor.Add(bulkRequest, visibilityTaskKey) 320 321 // processorAckTimeout is a maximum duration for bulk processor to commit the bulk and unblock the `ackF`. 322 // Default value is 30s and this timeout should never have happened, 323 // because Elasticsearch must process a bulk within 30s. 324 // Parent context is not respected here because it has shorter timeout (3s), 325 // which might already expired here due to wait at Add method above. 326 ctx, cancel := context.WithTimeout(context.Background(), s.processorAckTimeout()) 327 defer cancel() 328 ack, err := ackF.Get(ctx) 329 330 if err != nil { 331 if errors.Is(err, context.DeadlineExceeded) { 332 return &persistence.TimeoutError{Msg: fmt.Sprintf("visibility task %s timed out waiting for ACK after %v", visibilityTaskKey, s.processorAckTimeout())} 333 } 334 // Returns non-retryable Internal error here because these errors are unexpected. 335 // Visibility task processor retries all errors though, therefore new request will be generated for the same visibility task. 336 return serviceerror.NewInternal(fmt.Sprintf("visibility task %s received error %v", visibilityTaskKey, err)) 337 } 338 339 if !ack { 340 // Returns retryable Unavailable error here because NACK from bulk processor 341 // means that this request wasn't processed successfully and needs to be retried. 342 // Visibility task processor retries all errors anyway, therefore new request will be generated for the same visibility task. 343 return serviceerror.NewUnavailable(fmt.Sprintf("visibility task %s received NACK", visibilityTaskKey)) 344 } 345 return nil 346 } 347 348 func (s *visibilityStore) checkProcessor() { 349 if s.processor == nil { 350 // must be bug, check history setup 351 panic("Elasticsearch processor is nil") 352 } 353 if s.processorAckTimeout == nil { 354 // must be bug, check history setup 355 panic("config.ESProcessorAckTimeout is nil") 356 } 357 } 358 359 func (s *visibilityStore) ListOpenWorkflowExecutions( 360 ctx context.Context, 361 request *manager.ListWorkflowExecutionsRequest, 362 ) (*store.InternalListWorkflowExecutionsResponse, error) { 363 364 boolQuery := elastic.NewBoolQuery(). 365 Filter(elastic.NewTermQuery(searchattribute.ExecutionStatus, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String())) 366 367 p, err := s.buildSearchParameters(request, boolQuery, true) 368 if err != nil { 369 return nil, err 370 } 371 372 searchResult, err := s.esClient.Search(ctx, p) 373 if err != nil { 374 return nil, convertElasticsearchClientError("ListOpenWorkflowExecutions failed", err) 375 } 376 377 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 378 } 379 380 func (s *visibilityStore) ListClosedWorkflowExecutions( 381 ctx context.Context, 382 request *manager.ListWorkflowExecutionsRequest, 383 ) (*store.InternalListWorkflowExecutionsResponse, error) { 384 385 boolQuery := elastic.NewBoolQuery(). 386 MustNot(elastic.NewTermQuery(searchattribute.ExecutionStatus, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String())) 387 388 p, err := s.buildSearchParameters(request, boolQuery, false) 389 if err != nil { 390 return nil, err 391 } 392 393 searchResult, err := s.esClient.Search(ctx, p) 394 if err != nil { 395 return nil, convertElasticsearchClientError("ListClosedWorkflowExecutions failed", err) 396 } 397 398 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 399 } 400 401 func (s *visibilityStore) ListOpenWorkflowExecutionsByType( 402 ctx context.Context, 403 request *manager.ListWorkflowExecutionsByTypeRequest, 404 ) (*store.InternalListWorkflowExecutionsResponse, error) { 405 406 boolQuery := elastic.NewBoolQuery(). 407 Filter( 408 elastic.NewTermQuery(searchattribute.WorkflowType, request.WorkflowTypeName), 409 elastic.NewTermQuery(searchattribute.ExecutionStatus, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String())) 410 411 p, err := s.buildSearchParameters(request.ListWorkflowExecutionsRequest, boolQuery, true) 412 if err != nil { 413 return nil, err 414 } 415 416 searchResult, err := s.esClient.Search(ctx, p) 417 if err != nil { 418 return nil, convertElasticsearchClientError("ListOpenWorkflowExecutionsByType failed", err) 419 } 420 421 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 422 } 423 424 func (s *visibilityStore) ListClosedWorkflowExecutionsByType( 425 ctx context.Context, 426 request *manager.ListWorkflowExecutionsByTypeRequest, 427 ) (*store.InternalListWorkflowExecutionsResponse, error) { 428 429 boolQuery := elastic.NewBoolQuery(). 430 Filter(elastic.NewTermQuery(searchattribute.WorkflowType, request.WorkflowTypeName)). 431 MustNot(elastic.NewTermQuery(searchattribute.ExecutionStatus, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String())) 432 433 p, err := s.buildSearchParameters(request.ListWorkflowExecutionsRequest, boolQuery, false) 434 if err != nil { 435 return nil, err 436 } 437 438 searchResult, err := s.esClient.Search(ctx, p) 439 if err != nil { 440 return nil, convertElasticsearchClientError("ListClosedWorkflowExecutionsByType failed", err) 441 } 442 443 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 444 } 445 446 func (s *visibilityStore) ListOpenWorkflowExecutionsByWorkflowID( 447 ctx context.Context, 448 request *manager.ListWorkflowExecutionsByWorkflowIDRequest, 449 ) (*store.InternalListWorkflowExecutionsResponse, error) { 450 451 boolQuery := elastic.NewBoolQuery(). 452 Filter( 453 elastic.NewTermQuery(searchattribute.WorkflowID, request.WorkflowID), 454 elastic.NewTermQuery(searchattribute.ExecutionStatus, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String())) 455 456 p, err := s.buildSearchParameters(request.ListWorkflowExecutionsRequest, boolQuery, true) 457 if err != nil { 458 return nil, err 459 } 460 461 searchResult, err := s.esClient.Search(ctx, p) 462 if err != nil { 463 return nil, convertElasticsearchClientError("ListOpenWorkflowExecutionsByWorkflowID failed", err) 464 } 465 466 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 467 } 468 469 func (s *visibilityStore) ListClosedWorkflowExecutionsByWorkflowID( 470 ctx context.Context, 471 request *manager.ListWorkflowExecutionsByWorkflowIDRequest, 472 ) (*store.InternalListWorkflowExecutionsResponse, error) { 473 474 boolQuery := elastic.NewBoolQuery(). 475 Filter(elastic.NewTermQuery(searchattribute.WorkflowID, request.WorkflowID)). 476 MustNot(elastic.NewTermQuery(searchattribute.ExecutionStatus, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String())) 477 478 p, err := s.buildSearchParameters(request.ListWorkflowExecutionsRequest, boolQuery, false) 479 if err != nil { 480 return nil, err 481 } 482 483 searchResult, err := s.esClient.Search(ctx, p) 484 if err != nil { 485 return nil, convertElasticsearchClientError("ListClosedWorkflowExecutionsByWorkflowID failed", err) 486 } 487 488 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 489 } 490 491 func (s *visibilityStore) ListClosedWorkflowExecutionsByStatus( 492 ctx context.Context, 493 request *manager.ListClosedWorkflowExecutionsByStatusRequest, 494 ) (*store.InternalListWorkflowExecutionsResponse, error) { 495 496 boolQuery := elastic.NewBoolQuery(). 497 Filter(elastic.NewTermQuery(searchattribute.ExecutionStatus, request.Status.String())) 498 499 p, err := s.buildSearchParameters(request.ListWorkflowExecutionsRequest, boolQuery, false) 500 if err != nil { 501 return nil, err 502 } 503 504 searchResult, err := s.esClient.Search(ctx, p) 505 if err != nil { 506 return nil, convertElasticsearchClientError("ListClosedWorkflowExecutionsByStatus failed", err) 507 } 508 509 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 510 } 511 512 func (s *visibilityStore) ListWorkflowExecutions( 513 ctx context.Context, 514 request *manager.ListWorkflowExecutionsRequestV2, 515 ) (*store.InternalListWorkflowExecutionsResponse, error) { 516 p, err := s.buildSearchParametersV2(request, s.getListFieldSorter) 517 if err != nil { 518 return nil, err 519 } 520 521 searchResult, err := s.esClient.Search(ctx, p) 522 if err != nil { 523 return nil, convertElasticsearchClientError("ListWorkflowExecutions failed", err) 524 } 525 526 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 527 } 528 529 func (s *visibilityStore) ScanWorkflowExecutions( 530 ctx context.Context, 531 request *manager.ListWorkflowExecutionsRequestV2, 532 ) (*store.InternalListWorkflowExecutionsResponse, error) { 533 // Point in time is only supported in Elasticsearch 7.10+ in default flavor. 534 if s.esClient.IsPointInTimeSupported(ctx) { 535 return s.scanWorkflowExecutionsWithPit(ctx, request) 536 } 537 return s.scanWorkflowExecutionsWithScroll(ctx, request) 538 } 539 540 func (s *visibilityStore) scanWorkflowExecutionsWithScroll( 541 ctx context.Context, 542 request *manager.ListWorkflowExecutionsRequestV2, 543 ) (*store.InternalListWorkflowExecutionsResponse, error) { 544 var ( 545 searchResult *elastic.SearchResult 546 scrollErr error 547 ) 548 549 p, err := s.buildSearchParametersV2(request, s.getScanFieldSorter) 550 if err != nil { 551 return nil, err 552 } 553 554 if len(request.NextPageToken) == 0 { 555 searchResult, scrollErr = s.esClient.OpenScroll(ctx, p, scrollKeepAliveInterval) 556 } else if p.ScrollID != "" { 557 searchResult, scrollErr = s.esClient.Scroll(ctx, p.ScrollID, scrollKeepAliveInterval) 558 } else { 559 return nil, serviceerror.NewInvalidArgument("scrollId must present in pagination token") 560 } 561 562 if scrollErr != nil && scrollErr != io.EOF { 563 return nil, convertElasticsearchClientError("ScanWorkflowExecutions failed", scrollErr) 564 } 565 566 // Both io.IOF and empty hits list indicate that this is a last page. 567 if (searchResult.Hits != nil && len(searchResult.Hits.Hits) < request.PageSize) || 568 scrollErr == io.EOF { 569 err := s.esClient.CloseScroll(ctx, searchResult.ScrollId) 570 if err != nil { 571 return nil, convertElasticsearchClientError("Unable to close scroll", err) 572 } 573 } 574 575 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 576 } 577 578 func (s *visibilityStore) scanWorkflowExecutionsWithPit( 579 ctx context.Context, 580 request *manager.ListWorkflowExecutionsRequestV2, 581 ) (*store.InternalListWorkflowExecutionsResponse, error) { 582 p, err := s.buildSearchParametersV2(request, s.getScanFieldSorter) 583 if err != nil { 584 return nil, err 585 } 586 587 // First call doesn't have token with PointInTimeID. 588 if len(request.NextPageToken) == 0 { 589 pitID, err := s.esClient.OpenPointInTime(ctx, s.index, pointInTimeKeepAliveInterval) 590 if err != nil { 591 return nil, convertElasticsearchClientError("Unable to create point in time", err) 592 } 593 p.PointInTime = elastic.NewPointInTimeWithKeepAlive(pitID, pointInTimeKeepAliveInterval) 594 } else if p.PointInTime == nil { 595 return nil, serviceerror.NewInvalidArgument("pointInTimeId must present in pagination token") 596 } 597 598 searchResult, err := s.esClient.Search(ctx, p) 599 if err != nil { 600 return nil, convertElasticsearchClientError("ScanWorkflowExecutions failed", err) 601 } 602 603 // Number hits smaller than the page size indicate that this is the last page. 604 if searchResult.Hits != nil && len(searchResult.Hits.Hits) < request.PageSize { 605 _, err := s.esClient.ClosePointInTime(ctx, searchResult.PitId) 606 if err != nil { 607 return nil, convertElasticsearchClientError("Unable to close point in time", err) 608 } 609 } 610 611 return s.getListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize) 612 } 613 614 func (s *visibilityStore) CountWorkflowExecutions( 615 ctx context.Context, 616 request *manager.CountWorkflowExecutionsRequest, 617 ) (*manager.CountWorkflowExecutionsResponse, error) { 618 queryParams, err := s.convertQuery(request.Namespace, request.NamespaceID, request.Query) 619 if err != nil { 620 return nil, err 621 } 622 623 if len(queryParams.GroupBy) > 0 { 624 return s.countGroupByWorkflowExecutions(ctx, queryParams) 625 } 626 627 count, err := s.esClient.Count(ctx, s.index, queryParams.Query) 628 if err != nil { 629 return nil, convertElasticsearchClientError("CountWorkflowExecutions failed", err) 630 } 631 632 response := &manager.CountWorkflowExecutionsResponse{Count: count} 633 return response, nil 634 } 635 636 func (s *visibilityStore) countGroupByWorkflowExecutions( 637 ctx context.Context, 638 queryParams *query.QueryParams, 639 ) (*manager.CountWorkflowExecutionsResponse, error) { 640 groupByFields := queryParams.GroupBy 641 642 // Elasticsearch aggregation is nested. so need to loop backwards to build it. 643 // Example: when grouping by (field1, field2), the object looks like 644 // { 645 // "aggs": { 646 // "field1": { 647 // "terms": { 648 // "field": "field1" 649 // }, 650 // "aggs": { 651 // "field2": { 652 // "terms": { 653 // "field": "field2" 654 // } 655 // } 656 // } 657 // } 658 // } 659 // } 660 termsAgg := elastic.NewTermsAggregation().Field(groupByFields[len(groupByFields)-1]) 661 for i := len(groupByFields) - 2; i >= 0; i-- { 662 termsAgg = elastic.NewTermsAggregation(). 663 Field(groupByFields[i]). 664 SubAggregation(groupByFields[i+1], termsAgg) 665 } 666 esResponse, err := s.esClient.CountGroupBy( 667 ctx, 668 s.index, 669 queryParams.Query, 670 groupByFields[0], 671 termsAgg, 672 ) 673 if err != nil { 674 return nil, err 675 } 676 return s.parseCountGroupByResponse(esResponse, groupByFields) 677 } 678 679 func (s *visibilityStore) GetWorkflowExecution( 680 ctx context.Context, 681 request *manager.GetWorkflowExecutionRequest, 682 ) (*store.InternalGetWorkflowExecutionResponse, error) { 683 docID := getDocID(request.WorkflowID, request.RunID) 684 result, err := s.esClient.Get(ctx, s.index, docID) 685 if err != nil { 686 return nil, convertElasticsearchClientError("GetWorkflowExecution failed", err) 687 } 688 689 typeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.index, false) 690 if err != nil { 691 return nil, serviceerror.NewUnavailable( 692 fmt.Sprintf("Unable to read search attribute types: %v", err), 693 ) 694 } 695 696 if !result.Found { 697 return nil, serviceerror.NewNotFound( 698 fmt.Sprintf("Workflow execution with run id %s not found.", request.RunID), 699 ) 700 } 701 702 workflowExecutionInfo, err := s.parseESDoc(result.Id, result.Source, typeMap, request.Namespace) 703 if err != nil { 704 return nil, err 705 } 706 707 return &store.InternalGetWorkflowExecutionResponse{ 708 Execution: workflowExecutionInfo, 709 }, nil 710 } 711 712 func (s *visibilityStore) buildSearchParameters( 713 request *manager.ListWorkflowExecutionsRequest, 714 boolQuery *elastic.BoolQuery, 715 overStartTime bool, 716 ) (*client.SearchParameters, error) { 717 718 token, err := s.deserializePageToken(request.NextPageToken) 719 if err != nil { 720 return nil, err 721 } 722 723 boolQuery.Filter(elastic.NewTermQuery(searchattribute.NamespaceID, request.NamespaceID.String())) 724 725 if request.NamespaceDivision == "" { 726 boolQuery.MustNot(elastic.NewExistsQuery(searchattribute.TemporalNamespaceDivision)) 727 } else { 728 boolQuery.Filter(elastic.NewTermQuery(searchattribute.TemporalNamespaceDivision, request.NamespaceDivision)) 729 } 730 731 if !request.EarliestStartTime.IsZero() || !request.LatestStartTime.IsZero() { 732 var rangeQuery *elastic.RangeQuery 733 if overStartTime { 734 rangeQuery = elastic.NewRangeQuery(searchattribute.StartTime) 735 } else { 736 rangeQuery = elastic.NewRangeQuery(searchattribute.CloseTime) 737 } 738 739 if !request.EarliestStartTime.IsZero() { 740 rangeQuery = rangeQuery.Gte(request.EarliestStartTime) 741 } 742 743 if !request.LatestStartTime.IsZero() { 744 rangeQuery = rangeQuery.Lte(request.LatestStartTime) 745 } 746 boolQuery.Filter(rangeQuery) 747 } 748 749 params := &client.SearchParameters{ 750 Index: s.index, 751 Query: boolQuery, 752 PageSize: request.PageSize, 753 Sorter: defaultSorter, 754 } 755 756 if token != nil && len(token.SearchAfter) > 0 { 757 params.SearchAfter = token.SearchAfter 758 } 759 760 return params, nil 761 } 762 763 func (s *visibilityStore) buildSearchParametersV2( 764 request *manager.ListWorkflowExecutionsRequestV2, 765 getFieldSorter func([]elastic.Sorter) ([]elastic.Sorter, error), 766 ) (*client.SearchParameters, error) { 767 queryParams, err := s.convertQuery( 768 request.Namespace, 769 request.NamespaceID, 770 request.Query, 771 ) 772 if err != nil { 773 return nil, err 774 } 775 776 searchParams := &client.SearchParameters{ 777 Index: s.index, 778 PageSize: request.PageSize, 779 Query: queryParams.Query, 780 } 781 782 if len(queryParams.GroupBy) > 0 { 783 return nil, serviceerror.NewInvalidArgument("GROUP BY clause is not supported") 784 } 785 786 // TODO(rodrigozhou): investigate possible solutions to slow ORDER BY. 787 // ORDER BY clause can be slow if there is a large number of documents and 788 // using a field that was not indexed by ES. Since slow queries can block 789 // writes for unreasonably long, this option forbids the usage of ORDER BY 790 // clause to prevent slow down issues. 791 if s.disableOrderByClause(request.Namespace.String()) && len(queryParams.Sorter) > 0 { 792 return nil, serviceerror.NewInvalidArgument("ORDER BY clause is not supported") 793 } 794 795 if len(queryParams.Sorter) > 0 { 796 // If params.Sorter is not empty, then it's using custom order by. 797 s.metricsHandler.WithTags(metrics.NamespaceTag(request.Namespace.String())). 798 Counter(metrics.ElasticsearchCustomOrderByClauseCount.Name()).Record(1) 799 } 800 801 searchParams.Sorter, err = getFieldSorter(queryParams.Sorter) 802 if err != nil { 803 return nil, err 804 } 805 806 pageToken, err := s.deserializePageToken(request.NextPageToken) 807 if err != nil { 808 return nil, err 809 } 810 err = s.processPageToken(searchParams, pageToken, request.Namespace) 811 if err != nil { 812 return nil, err 813 } 814 815 return searchParams, nil 816 } 817 818 func (s *visibilityStore) processPageToken( 819 params *client.SearchParameters, 820 pageToken *visibilityPageToken, 821 namespaceName namespace.Name, 822 ) error { 823 if pageToken == nil { 824 return nil 825 } 826 if pageToken.ScrollID != "" { 827 params.ScrollID = pageToken.ScrollID 828 return nil 829 } 830 if len(pageToken.SearchAfter) == 0 { 831 return nil 832 } 833 if pageToken.PointInTimeID != "" { 834 params.SearchAfter = pageToken.SearchAfter 835 params.PointInTime = elastic.NewPointInTimeWithKeepAlive( 836 pageToken.PointInTimeID, 837 pointInTimeKeepAliveInterval, 838 ) 839 return nil 840 } 841 if len(pageToken.SearchAfter) != len(params.Sorter) { 842 return serviceerror.NewInvalidArgument(fmt.Sprintf( 843 "Invalid page token for given sort fields: expected %d fields, got %d", 844 len(params.Sorter), 845 len(pageToken.SearchAfter), 846 )) 847 } 848 if !s.enableManualPagination(namespaceName.String()) || !isDefaultSorter(params.Sorter) { 849 params.SearchAfter = pageToken.SearchAfter 850 return nil 851 } 852 853 boolQuery, ok := params.Query.(*elastic.BoolQuery) 854 if !ok { 855 return serviceerror.NewInternal(fmt.Sprintf( 856 "Unexpected query type: expected *elastic.BoolQuery, got %T", 857 params.Query, 858 )) 859 } 860 861 saTypeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.index, false) 862 if err != nil { 863 return serviceerror.NewUnavailable( 864 fmt.Sprintf("Unable to read search attribute types: %v", err), 865 ) 866 } 867 868 // build pagination search query for default sorter 869 shouldQueries, err := buildPaginationQuery(defaultSorterFields, pageToken.SearchAfter, saTypeMap) 870 if err != nil { 871 return err 872 } 873 874 boolQuery.Should(shouldQueries...) 875 boolQuery.MinimumNumberShouldMatch(1) 876 return nil 877 } 878 879 func (s *visibilityStore) convertQuery( 880 namespace namespace.Name, 881 namespaceID namespace.ID, 882 requestQueryStr string, 883 ) (*query.QueryParams, error) { 884 saTypeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.index, false) 885 if err != nil { 886 return nil, serviceerror.NewUnavailable(fmt.Sprintf("Unable to read search attribute types: %v", err)) 887 } 888 nameInterceptor := newNameInterceptor(namespace, s.index, saTypeMap, s.searchAttributesMapperProvider) 889 queryConverter := newQueryConverter( 890 nameInterceptor, 891 NewValuesInterceptor(namespace, saTypeMap, s.searchAttributesMapperProvider), 892 ) 893 queryParams, err := queryConverter.ConvertWhereOrderBy(requestQueryStr) 894 if err != nil { 895 // Convert ConverterError to InvalidArgument and pass through all other errors (which should be only mapper errors). 896 var converterErr *query.ConverterError 897 if errors.As(err, &converterErr) { 898 return nil, converterErr.ToInvalidArgument() 899 } 900 return nil, err 901 } 902 903 // Create new bool query because request query might have only "should" (="or") queries. 904 namespaceFilterQuery := elastic.NewBoolQuery().Filter(elastic.NewTermQuery(searchattribute.NamespaceID, namespaceID.String())) 905 906 // If the query did not explicitly filter on TemporalNamespaceDivision somehow, then add a 907 // "must not exist" (i.e. "is null") query for it. 908 if !nameInterceptor.seenNamespaceDivision { 909 namespaceFilterQuery.MustNot(elastic.NewExistsQuery(searchattribute.TemporalNamespaceDivision)) 910 } 911 912 if queryParams.Query != nil { 913 namespaceFilterQuery.Filter(queryParams.Query) 914 } 915 916 queryParams.Query = namespaceFilterQuery 917 return queryParams, nil 918 } 919 920 func (s *visibilityStore) getScanFieldSorter(fieldSorts []elastic.Sorter) ([]elastic.Sorter, error) { 921 // custom order is not supported by Scan API 922 if len(fieldSorts) > 0 { 923 return nil, serviceerror.NewInvalidArgument("ORDER BY clause is not supported") 924 } 925 926 return docSorter, nil 927 } 928 929 func (s *visibilityStore) getListFieldSorter(fieldSorts []elastic.Sorter) ([]elastic.Sorter, error) { 930 if len(fieldSorts) == 0 { 931 return defaultSorter, nil 932 } 933 res := make([]elastic.Sorter, len(fieldSorts)+1) 934 for i, fs := range fieldSorts { 935 res[i] = fs 936 } 937 // RunID is explicit tiebreaker. 938 res[len(res)-1] = elastic.NewFieldSort(searchattribute.RunID).Desc() 939 940 return res, nil 941 } 942 943 func (s *visibilityStore) getListWorkflowExecutionsResponse( 944 searchResult *elastic.SearchResult, 945 namespace namespace.Name, 946 pageSize int, 947 ) (*store.InternalListWorkflowExecutionsResponse, error) { 948 949 if searchResult.Hits == nil || len(searchResult.Hits.Hits) == 0 { 950 return &store.InternalListWorkflowExecutionsResponse{}, nil 951 } 952 953 typeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.index, false) 954 if err != nil { 955 return nil, serviceerror.NewUnavailable(fmt.Sprintf("Unable to read search attribute types: %v", err)) 956 } 957 958 response := &store.InternalListWorkflowExecutionsResponse{ 959 Executions: make([]*store.InternalWorkflowExecutionInfo, 0, len(searchResult.Hits.Hits)), 960 } 961 var lastHitSort []interface{} 962 for _, hit := range searchResult.Hits.Hits { 963 workflowExecutionInfo, err := s.parseESDoc(hit.Id, hit.Source, typeMap, namespace) 964 if err != nil { 965 return nil, err 966 } 967 response.Executions = append(response.Executions, workflowExecutionInfo) 968 lastHitSort = hit.Sort 969 } 970 971 if len(searchResult.Hits.Hits) == pageSize { // this means the response might not the last page 972 response.NextPageToken, err = s.serializePageToken(&visibilityPageToken{ 973 SearchAfter: lastHitSort, 974 ScrollID: searchResult.ScrollId, 975 PointInTimeID: searchResult.PitId, 976 }) 977 if err != nil { 978 return nil, err 979 } 980 } 981 982 return response, nil 983 } 984 985 func (s *visibilityStore) deserializePageToken(data []byte) (*visibilityPageToken, error) { 986 if len(data) == 0 { 987 return nil, nil 988 } 989 990 var token *visibilityPageToken 991 dec := json.NewDecoder(bytes.NewReader(data)) 992 // UseNumber will not lose precision on big int64. 993 dec.UseNumber() 994 err := dec.Decode(&token) 995 if err != nil { 996 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unable to deserialize page token: %v", err)) 997 } 998 return token, nil 999 } 1000 1001 func (s *visibilityStore) serializePageToken(token *visibilityPageToken) ([]byte, error) { 1002 if token == nil { 1003 return nil, nil 1004 } 1005 1006 data, err := json.Marshal(token) 1007 if err != nil { 1008 return nil, serviceerror.NewInternal(fmt.Sprintf("unable to serialize page token: %v", err)) 1009 } 1010 return data, nil 1011 } 1012 1013 func (s *visibilityStore) generateESDoc( 1014 request *store.InternalVisibilityRequestBase, 1015 visibilityTaskKey string, 1016 ) (map[string]interface{}, error) { 1017 doc := map[string]interface{}{ 1018 searchattribute.VisibilityTaskKey: visibilityTaskKey, 1019 searchattribute.NamespaceID: request.NamespaceID, 1020 searchattribute.WorkflowID: request.WorkflowID, 1021 searchattribute.RunID: request.RunID, 1022 searchattribute.WorkflowType: request.WorkflowTypeName, 1023 searchattribute.StartTime: request.StartTime, 1024 searchattribute.ExecutionTime: request.ExecutionTime, 1025 searchattribute.ExecutionStatus: request.Status.String(), 1026 searchattribute.TaskQueue: request.TaskQueue, 1027 } 1028 1029 if request.ParentWorkflowID != nil { 1030 doc[searchattribute.ParentWorkflowID] = *request.ParentWorkflowID 1031 } 1032 if request.ParentRunID != nil { 1033 doc[searchattribute.ParentRunID] = *request.ParentRunID 1034 } 1035 1036 if len(request.Memo.GetData()) > 0 { 1037 doc[searchattribute.Memo] = request.Memo.GetData() 1038 doc[searchattribute.MemoEncoding] = request.Memo.GetEncodingType().String() 1039 } 1040 1041 typeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.index, false) 1042 if err != nil { 1043 s.metricsHandler.Counter(metrics.ElasticsearchDocumentGenerateFailuresCount.Name()).Record(1) 1044 return nil, serviceerror.NewUnavailable(fmt.Sprintf("Unable to read search attribute types: %v", err)) 1045 } 1046 1047 searchAttributes, err := searchattribute.Decode(request.SearchAttributes, &typeMap, true) 1048 if err != nil { 1049 s.metricsHandler.Counter(metrics.ElasticsearchDocumentGenerateFailuresCount.Name()).Record(1) 1050 return nil, serviceerror.NewInternal(fmt.Sprintf("Unable to decode search attributes: %v", err)) 1051 } 1052 // This is to prevent existing tasks to fail indefinitely. 1053 // If it's only invalid values error, then silently continue without them. 1054 searchAttributes, err = s.ValidateCustomSearchAttributes(searchAttributes) 1055 if err != nil { 1056 if _, ok := err.(*store.VisibilityStoreInvalidValuesError); !ok { 1057 return nil, err 1058 } 1059 } 1060 for saName, saValue := range searchAttributes { 1061 if saValue == nil { 1062 // If search attribute value is `nil`, it means that it shouldn't be added to the document. 1063 // Empty slices are converted to `nil` while decoding. 1064 continue 1065 } 1066 doc[saName] = saValue 1067 } 1068 1069 return doc, nil 1070 } 1071 1072 func (s *visibilityStore) parseESDoc(docID string, docSource json.RawMessage, saTypeMap searchattribute.NameTypeMap, namespace namespace.Name) (*store.InternalWorkflowExecutionInfo, error) { 1073 logParseError := func(fieldName string, fieldValue interface{}, err error, docID string) error { 1074 s.metricsHandler.Counter(metrics.ElasticsearchDocumentParseFailuresCount.Name()).Record(1) 1075 return serviceerror.NewInternal(fmt.Sprintf("Unable to parse Elasticsearch document(%s) %q field value %q: %v", docID, fieldName, fieldValue, err)) 1076 } 1077 1078 var sourceMap map[string]interface{} 1079 d := json.NewDecoder(bytes.NewReader(docSource)) 1080 // Very important line. See finishParseJSONValue bellow. 1081 d.UseNumber() 1082 if err := d.Decode(&sourceMap); err != nil { 1083 s.metricsHandler.Counter(metrics.ElasticsearchDocumentParseFailuresCount.Name()).Record(1) 1084 return nil, serviceerror.NewInternal(fmt.Sprintf("Unable to unmarshal JSON from Elasticsearch document(%s): %v", docID, err)) 1085 } 1086 1087 var ( 1088 isValidType bool 1089 memo []byte 1090 memoEncoding string 1091 customSearchAttributes map[string]interface{} 1092 ) 1093 record := &store.InternalWorkflowExecutionInfo{} 1094 for fieldName, fieldValue := range sourceMap { 1095 switch fieldName { 1096 case searchattribute.NamespaceID, 1097 searchattribute.ExecutionDuration, 1098 searchattribute.VisibilityTaskKey: 1099 // Ignore these fields. 1100 continue 1101 case searchattribute.Memo: 1102 var memoStr string 1103 if memoStr, isValidType = fieldValue.(string); !isValidType { 1104 return nil, logParseError(fieldName, fieldValue, fmt.Errorf("%w: expected string got %T", errUnexpectedJSONFieldType, fieldValue), docID) 1105 } 1106 var err error 1107 if memo, err = base64.StdEncoding.DecodeString(memoStr); err != nil { 1108 return nil, logParseError(fieldName, memoStr[:10], err, docID) 1109 } 1110 continue 1111 case searchattribute.MemoEncoding: 1112 if memoEncoding, isValidType = fieldValue.(string); !isValidType { 1113 return nil, logParseError(fieldName, fieldValue, fmt.Errorf("%w: expected string got %T", errUnexpectedJSONFieldType, fieldValue), docID) 1114 } 1115 continue 1116 } 1117 1118 fieldType, err := saTypeMap.GetType(fieldName) 1119 if err != nil { 1120 // Silently ignore ErrInvalidName because it indicates unknown field in Elasticsearch document. 1121 if errors.Is(err, searchattribute.ErrInvalidName) { 1122 continue 1123 } 1124 s.metricsHandler.Counter(metrics.ElasticsearchDocumentParseFailuresCount.Name()).Record(1) 1125 return nil, serviceerror.NewInternal(fmt.Sprintf("Unable to get type for Elasticsearch document(%s) field %q: %v", docID, fieldName, err)) 1126 } 1127 1128 fieldValueParsed, err := finishParseJSONValue(fieldValue, fieldType) 1129 if err != nil { 1130 return nil, logParseError(fieldName, fieldValue, err, docID) 1131 } 1132 1133 switch fieldName { 1134 case searchattribute.WorkflowID: 1135 record.WorkflowID = fieldValueParsed.(string) 1136 case searchattribute.RunID: 1137 record.RunID = fieldValueParsed.(string) 1138 case searchattribute.WorkflowType: 1139 record.TypeName = fieldValue.(string) 1140 case searchattribute.StartTime: 1141 record.StartTime = fieldValueParsed.(time.Time) 1142 case searchattribute.ExecutionTime: 1143 record.ExecutionTime = fieldValueParsed.(time.Time) 1144 case searchattribute.CloseTime: 1145 record.CloseTime = fieldValueParsed.(time.Time) 1146 case searchattribute.TaskQueue: 1147 record.TaskQueue = fieldValueParsed.(string) 1148 case searchattribute.ExecutionStatus: 1149 status, err := enumspb.WorkflowExecutionStatusFromString(fieldValueParsed.(string)) 1150 if err != nil { 1151 return nil, logParseError(fieldName, fieldValueParsed.(string), err, docID) 1152 } 1153 record.Status = status 1154 case searchattribute.HistoryLength: 1155 record.HistoryLength = fieldValueParsed.(int64) 1156 case searchattribute.StateTransitionCount: 1157 record.StateTransitionCount = fieldValueParsed.(int64) 1158 case searchattribute.HistorySizeBytes: 1159 record.HistorySizeBytes = fieldValueParsed.(int64) 1160 case searchattribute.ParentWorkflowID: 1161 record.ParentWorkflowID = fieldValueParsed.(string) 1162 case searchattribute.ParentRunID: 1163 record.ParentRunID = fieldValueParsed.(string) 1164 default: 1165 // All custom and predefined search attributes are handled here. 1166 if customSearchAttributes == nil { 1167 customSearchAttributes = map[string]interface{}{} 1168 } 1169 customSearchAttributes[fieldName] = fieldValueParsed 1170 } 1171 } 1172 1173 if customSearchAttributes != nil { 1174 var err error 1175 record.SearchAttributes, err = searchattribute.Encode(customSearchAttributes, &saTypeMap) 1176 if err != nil { 1177 s.metricsHandler.Counter(metrics.ElasticsearchDocumentParseFailuresCount.Name()).Record(1) 1178 return nil, serviceerror.NewInternal(fmt.Sprintf("Unable to encode custom search attributes of Elasticsearch document(%s): %v", docID, err)) 1179 } 1180 aliasedSas, err := searchattribute.AliasFields(s.searchAttributesMapperProvider, record.SearchAttributes, namespace.String()) 1181 if err != nil { 1182 return nil, err 1183 } 1184 1185 if aliasedSas != nil { 1186 record.SearchAttributes = aliasedSas 1187 } 1188 } 1189 1190 if memoEncoding != "" { 1191 record.Memo = persistence.NewDataBlob(memo, memoEncoding) 1192 } else if memo != nil { 1193 s.metricsHandler.Counter(metrics.ElasticsearchDocumentParseFailuresCount.Name()).Record(1) 1194 return nil, serviceerror.NewInternal(fmt.Sprintf("%q field is missing in Elasticsearch document(%s)", searchattribute.MemoEncoding, docID)) 1195 } 1196 1197 return record, nil 1198 } 1199 1200 // Elasticsearch aggregation groups are returned as nested object. 1201 // This function flattens the response into rows. 1202 // 1203 //nolint:revive // cognitive complexity 27 (> max enabled 25) 1204 func (s *visibilityStore) parseCountGroupByResponse( 1205 searchResult *elastic.SearchResult, 1206 groupByFields []string, 1207 ) (*manager.CountWorkflowExecutionsResponse, error) { 1208 response := &manager.CountWorkflowExecutionsResponse{} 1209 typeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.index, false) 1210 if err != nil { 1211 return nil, serviceerror.NewUnavailable( 1212 fmt.Sprintf("Unable to read search attribute types: %v", err), 1213 ) 1214 } 1215 groupByTypes := make([]enumspb.IndexedValueType, len(groupByFields)) 1216 for i, saName := range groupByFields { 1217 tp, err := typeMap.GetType(saName) 1218 if err != nil { 1219 return nil, err 1220 } 1221 groupByTypes[i] = tp 1222 } 1223 1224 parseJsonNumber := func(val any) (int64, error) { 1225 numberVal, isNumber := val.(json.Number) 1226 if !isNumber { 1227 return 0, fmt.Errorf("%w: expected json.Number, got %T", errUnexpectedJSONFieldType, val) 1228 } 1229 return numberVal.Int64() 1230 } 1231 1232 var parseInternal func(map[string]any, []*commonpb.Payload) error 1233 parseInternal = func(aggs map[string]any, bucketValues []*commonpb.Payload) error { 1234 if len(bucketValues) == len(groupByFields) { 1235 cnt, err := parseJsonNumber(aggs["doc_count"]) 1236 if err != nil { 1237 return fmt.Errorf("Unable to parse 'doc_count' field: %w", err) 1238 } 1239 groupValues := make([]*commonpb.Payload, len(groupByFields)) 1240 for i := range bucketValues { 1241 groupValues[i] = bucketValues[i] 1242 } 1243 response.Groups = append( 1244 response.Groups, 1245 &workflowservice.CountWorkflowExecutionsResponse_AggregationGroup{ 1246 GroupValues: groupValues, 1247 Count: cnt, 1248 }, 1249 ) 1250 response.Count += cnt 1251 return nil 1252 } 1253 1254 index := len(bucketValues) 1255 fieldName := groupByFields[index] 1256 buckets := aggs[fieldName].(map[string]any)["buckets"].([]any) 1257 for i := range buckets { 1258 bucket := buckets[i].(map[string]any) 1259 value, err := finishParseJSONValue(bucket["key"], groupByTypes[index]) 1260 if err != nil { 1261 return fmt.Errorf("Failed to parse value %v: %w", bucket["key"], err) 1262 } 1263 payload, err := searchattribute.EncodeValue(value, groupByTypes[index]) 1264 if err != nil { 1265 return fmt.Errorf("Failed to encode value %v: %w", value, err) 1266 } 1267 err = parseInternal(bucket, append(bucketValues, payload)) 1268 if err != nil { 1269 return err 1270 } 1271 } 1272 return nil 1273 } 1274 1275 var bucketsJson map[string]any 1276 dec := json.NewDecoder(bytes.NewReader(searchResult.Aggregations[groupByFields[0]])) 1277 dec.UseNumber() 1278 if err := dec.Decode(&bucketsJson); err != nil { 1279 return nil, serviceerror.NewInternal(fmt.Sprintf("unable to unmarshal json response: %v", err)) 1280 } 1281 if err := parseInternal(map[string]any{groupByFields[0]: bucketsJson}, nil); err != nil { 1282 return nil, err 1283 } 1284 return response, nil 1285 } 1286 1287 // finishParseJSONValue finishes JSON parsing after json.Decode. 1288 // json.Decode returns: 1289 // 1290 // bool, for JSON booleans 1291 // json.Number, for JSON numbers (because of d.UseNumber()) 1292 // string, for JSON strings 1293 // []interface{}, for JSON arrays 1294 // map[string]interface{}, for JSON objects (should never be a case) 1295 // nil for JSON null 1296 func finishParseJSONValue(val interface{}, t enumspb.IndexedValueType) (interface{}, error) { 1297 // Custom search attributes support array of particular type. 1298 if arrayValue, isArray := val.([]interface{}); isArray { 1299 retArray := make([]interface{}, len(arrayValue)) 1300 var lastErr error 1301 for i := 0; i < len(retArray); i++ { 1302 retArray[i], lastErr = finishParseJSONValue(arrayValue[i], t) 1303 } 1304 return retArray, lastErr 1305 } 1306 1307 switch t { 1308 case enumspb.INDEXED_VALUE_TYPE_TEXT, 1309 enumspb.INDEXED_VALUE_TYPE_KEYWORD, 1310 enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, 1311 enumspb.INDEXED_VALUE_TYPE_DATETIME: 1312 stringVal, isString := val.(string) 1313 if !isString { 1314 return nil, fmt.Errorf("%w: expected string got %T", errUnexpectedJSONFieldType, val) 1315 } 1316 if t == enumspb.INDEXED_VALUE_TYPE_DATETIME { 1317 return time.Parse(time.RFC3339Nano, stringVal) 1318 } 1319 return stringVal, nil 1320 case enumspb.INDEXED_VALUE_TYPE_INT, enumspb.INDEXED_VALUE_TYPE_DOUBLE: 1321 numberVal, isNumber := val.(json.Number) 1322 if !isNumber { 1323 return nil, fmt.Errorf("%w: expected json.Number got %T", errUnexpectedJSONFieldType, val) 1324 } 1325 if t == enumspb.INDEXED_VALUE_TYPE_INT { 1326 return numberVal.Int64() 1327 } 1328 return numberVal.Float64() 1329 case enumspb.INDEXED_VALUE_TYPE_BOOL: 1330 boolVal, isBool := val.(bool) 1331 if !isBool { 1332 return nil, fmt.Errorf("%w: expected bool got %T", errUnexpectedJSONFieldType, val) 1333 } 1334 return boolVal, nil 1335 } 1336 1337 panic(fmt.Sprintf("Unknown field type: %v", t)) 1338 } 1339 1340 func convertElasticsearchClientError(message string, err error) error { 1341 errMessage := fmt.Sprintf("%s: %s", message, detailedErrorMessage(err)) 1342 switch e := err.(type) { 1343 case *elastic.Error: 1344 switch e.Status { 1345 case 400: // BadRequest 1346 // Returning InvalidArgument error will prevent retry on a caller side. 1347 return serviceerror.NewInvalidArgument(errMessage) 1348 } 1349 } 1350 return serviceerror.NewUnavailable(errMessage) 1351 } 1352 1353 func detailedErrorMessage(err error) string { 1354 var elasticErr *elastic.Error 1355 if !errors.As(err, &elasticErr) || 1356 elasticErr.Details == nil || 1357 len(elasticErr.Details.RootCause) == 0 || 1358 (len(elasticErr.Details.RootCause) == 1 && elasticErr.Details.RootCause[0].Reason == elasticErr.Details.Reason) { 1359 return err.Error() 1360 } 1361 1362 var sb strings.Builder 1363 sb.WriteString(elasticErr.Error()) 1364 sb.WriteString(", root causes:") 1365 for i, rootCause := range elasticErr.Details.RootCause { 1366 sb.WriteString(fmt.Sprintf(" %s [type=%s]", rootCause.Reason, rootCause.Type)) 1367 if i != len(elasticErr.Details.RootCause)-1 { 1368 sb.WriteRune(',') 1369 } 1370 } 1371 return sb.String() 1372 } 1373 1374 func isDefaultSorter(sorter []elastic.Sorter) bool { 1375 if len(sorter) != len(defaultSorter) { 1376 return false 1377 } 1378 for i := 0; i < len(defaultSorter); i++ { 1379 if &sorter[i] != &defaultSorter[i] { 1380 return false 1381 } 1382 } 1383 return true 1384 } 1385 1386 // buildPaginationQuery builds the Elasticsearch conditions for the next page based on searchAfter. 1387 // 1388 // For example, if sorterFields = [A, B, C] and searchAfter = [lastA, lastB, lastC], 1389 // it will build the following conditions (assuming all values are non-null and orders are desc): 1390 // - k = 0: A < lastA 1391 // - k = 1: A = lastA AND B < lastB 1392 // - k = 2: A = lastA AND B = lastB AND C < lastC 1393 // 1394 //nolint:revive // cyclomatic complexity 1395 func buildPaginationQuery( 1396 sorterFields []fieldSort, 1397 searchAfter []any, 1398 saTypeMap searchattribute.NameTypeMap, 1399 ) ([]elastic.Query, error) { 1400 n := len(sorterFields) 1401 if len(sorterFields) != len(searchAfter) { 1402 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1403 "Invalid page token for given sort fields: expected %d fields, got %d", 1404 len(sorterFields), 1405 len(searchAfter), 1406 )) 1407 } 1408 1409 parsedSearchAfter := make([]any, n) 1410 for i := 0; i < n; i++ { 1411 tp, err := saTypeMap.GetType(sorterFields[i].name) 1412 if err != nil { 1413 return nil, err 1414 } 1415 parsedSearchAfter[i], err = parsePageTokenValue(sorterFields[i].name, searchAfter[i], tp) 1416 if err != nil { 1417 return nil, err 1418 } 1419 } 1420 1421 // Last field of sorter must be a tie breaker, and thus cannot contain null value. 1422 if parsedSearchAfter[len(parsedSearchAfter)-1] == nil { 1423 return nil, serviceerror.NewInternal(fmt.Sprintf( 1424 "Last field of sorter cannot be a nullable field: %q has null values", 1425 sorterFields[len(sorterFields)-1].name, 1426 )) 1427 } 1428 1429 shouldQueries := make([]elastic.Query, 0, len(sorterFields)) 1430 for k := 0; k < len(sorterFields); k++ { 1431 bq := elastic.NewBoolQuery() 1432 for i := 0; i <= k; i++ { 1433 field := sorterFields[i] 1434 value := parsedSearchAfter[i] 1435 if i == k { 1436 if value == nil { 1437 bq.Filter(elastic.NewExistsQuery(field.name)) 1438 } else if field.desc { 1439 bq.Filter(elastic.NewRangeQuery(field.name).Lt(value)) 1440 } else { 1441 bq.Filter(elastic.NewRangeQuery(field.name).Gt(value)) 1442 } 1443 } else { 1444 if value == nil { 1445 bq.MustNot(elastic.NewExistsQuery(field.name)) 1446 } else { 1447 bq.Filter(elastic.NewTermQuery(field.name, value)) 1448 } 1449 } 1450 } 1451 shouldQueries = append(shouldQueries, bq) 1452 } 1453 return shouldQueries, nil 1454 } 1455 1456 // parsePageTokenValue parses the page token values to be used in the search query. 1457 // The page token comes from the `sort` field from the previous response from Elasticsearch. 1458 // Depending on the type of the field, the null value is represented differently: 1459 // - integer, bool, and datetime: MaxInt64 (desc) or MinInt64 (asc) 1460 // - double: "Infinity" (desc) or "-Infinity" (asc) 1461 // - keyword: nil 1462 // 1463 // Furthermore, for bool and datetime, they need to be converted to boolean or the RFC3339Nano 1464 // formats respectively. 1465 // 1466 //nolint:revive // cyclomatic complexity 1467 func parsePageTokenValue( 1468 fieldName string, jsonValue any, 1469 tp enumspb.IndexedValueType, 1470 ) (any, error) { 1471 switch tp { 1472 case enumspb.INDEXED_VALUE_TYPE_INT, 1473 enumspb.INDEXED_VALUE_TYPE_BOOL, 1474 enumspb.INDEXED_VALUE_TYPE_DATETIME: 1475 jsonNumber, ok := jsonValue.(json.Number) 1476 if !ok { 1477 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1478 "Invalid page token: expected interger type, got %q", jsonValue)) 1479 } 1480 num, err := jsonNumber.Int64() 1481 if err != nil { 1482 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1483 "Invalid page token: expected interger type, got %v", jsonValue)) 1484 } 1485 if num == math.MaxInt64 || num == math.MinInt64 { 1486 return nil, nil 1487 } 1488 if tp == enumspb.INDEXED_VALUE_TYPE_BOOL { 1489 return num != 0, nil 1490 } 1491 if tp == enumspb.INDEXED_VALUE_TYPE_DATETIME { 1492 return time.Unix(0, num).UTC().Format(time.RFC3339Nano), nil 1493 } 1494 return num, nil 1495 1496 case enumspb.INDEXED_VALUE_TYPE_DOUBLE: 1497 switch v := jsonValue.(type) { 1498 case json.Number: 1499 num, err := v.Float64() 1500 if err != nil { 1501 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1502 "Invalid page token: expected float type, got %v", jsonValue)) 1503 } 1504 return num, nil 1505 case string: 1506 // it can be the string representation of infinity 1507 if _, err := strconv.ParseFloat(v, 64); err != nil { 1508 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1509 "Invalid page token: expected float type, got %q", jsonValue)) 1510 } 1511 return nil, nil 1512 default: 1513 // it should never reach here 1514 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1515 "Invalid page token: expected float type, got %#v", jsonValue)) 1516 } 1517 1518 case enumspb.INDEXED_VALUE_TYPE_KEYWORD: 1519 if jsonValue == nil { 1520 return nil, nil 1521 } 1522 if _, ok := jsonValue.(string); !ok { 1523 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1524 "Invalid page token: expected string type, got %v", jsonValue)) 1525 } 1526 return jsonValue, nil 1527 1528 default: 1529 return nil, serviceerror.NewInvalidArgument(fmt.Sprintf( 1530 "Invalid field type in sorter: cannot order by %q", 1531 fieldName, 1532 )) 1533 } 1534 } 1535 1536 func validateDatetime(value time.Time) error { 1537 if value.Before(minTime) || value.After(maxTime) { 1538 return serviceerror.NewInvalidArgument( 1539 fmt.Sprintf("Date not supported in Elasticsearch: %v", value), 1540 ) 1541 } 1542 return nil 1543 } 1544 1545 func validateString(value string) error { 1546 if len(value) > maxStringLength { 1547 return serviceerror.NewInvalidArgument( 1548 fmt.Sprintf( 1549 "Strings with more than %d bytes are not supported in Elasticsearch (got %s)", 1550 maxStringLength, 1551 value, 1552 ), 1553 ) 1554 } 1555 return nil 1556 }