go.temporal.io/server@v1.23.0/common/persistence/visibility/store/elasticsearch/client/client_v7.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 client 26 27 import ( 28 "context" 29 "encoding/json" 30 "fmt" 31 "net/http" 32 "net/url" 33 "strings" 34 "sync" 35 "time" 36 37 "github.com/blang/semver/v4" 38 "github.com/olivere/elastic/v7" 39 "github.com/olivere/elastic/v7/uritemplates" 40 enumspb "go.temporal.io/api/enums/v1" 41 42 "go.temporal.io/server/common/auth" 43 "go.temporal.io/server/common/log" 44 ) 45 46 type ( 47 // clientImpl implements Client 48 clientImpl struct { 49 esClient *elastic.Client 50 url url.URL 51 52 initIsPointInTimeSupported sync.Once 53 isPointInTimeSupported bool 54 } 55 ) 56 57 const ( 58 pointInTimeSupportedFlavor = "default" // the other flavor is "oss" 59 ) 60 61 var ( 62 pointInTimeSupportedIn = semver.MustParseRange(">=7.10.0") 63 ) 64 65 var _ Client = (*clientImpl)(nil) 66 67 // newClient create a ES client 68 func newClient(cfg *Config, httpClient *http.Client, logger log.Logger) (*clientImpl, error) { 69 var urls []string 70 if len(cfg.URLs) > 0 { 71 urls = make([]string, len(cfg.URLs)) 72 for i, u := range cfg.URLs { 73 urls[i] = u.String() 74 } 75 } else { 76 urls = []string{cfg.URL.String()} 77 } 78 options := []elastic.ClientOptionFunc{ 79 elastic.SetURL(urls...), 80 elastic.SetBasicAuth(cfg.Username, cfg.Password), 81 // Disable healthcheck to prevent blocking client creation (and thus Temporal server startup) if the Elasticsearch is down. 82 elastic.SetHealthcheck(false), 83 elastic.SetSniff(cfg.EnableSniff), 84 elastic.SetRetrier(elastic.NewBackoffRetrier(elastic.NewExponentialBackoff(128*time.Millisecond, 513*time.Millisecond))), 85 // Critical to ensure decode of int64 won't lose precision. 86 elastic.SetDecoder(&elastic.NumberDecoder{}), 87 elastic.SetGzip(true), 88 } 89 90 options = append(options, getLoggerOptions(cfg.LogLevel, logger)...) 91 92 if httpClient == nil { 93 if cfg.TLS != nil && cfg.TLS.Enabled { 94 tlsHttpClient, err := buildTLSHTTPClient(cfg.TLS) 95 if err != nil { 96 return nil, fmt.Errorf("unable to create TLS HTTP client: %w", err) 97 } 98 httpClient = tlsHttpClient 99 } else { 100 httpClient = http.DefaultClient 101 } 102 } 103 104 // TODO (alex): Remove this when https://github.com/olivere/elastic/pull/1507 is merged. 105 if cfg.CloseIdleConnectionsInterval != time.Duration(0) { 106 if cfg.CloseIdleConnectionsInterval < minimumCloseIdleConnectionsInterval { 107 cfg.CloseIdleConnectionsInterval = minimumCloseIdleConnectionsInterval 108 } 109 go func(interval time.Duration, httpClient *http.Client) { 110 closeTimer := time.NewTimer(interval) 111 defer closeTimer.Stop() 112 for { 113 <-closeTimer.C 114 closeTimer.Reset(interval) 115 httpClient.CloseIdleConnections() 116 } 117 }(cfg.CloseIdleConnectionsInterval, httpClient) 118 } 119 120 options = append(options, elastic.SetHttpClient(httpClient)) 121 122 client, err := elastic.NewClient(options...) 123 if err != nil { 124 return nil, err 125 } 126 127 // Enable healthcheck (if configured) after client is successfully created. 128 if cfg.EnableHealthcheck { 129 client.Stop() 130 err = elastic.SetHealthcheck(true)(client) 131 if err != nil { 132 return nil, err 133 } 134 client.Start() 135 } 136 137 return &clientImpl{ 138 esClient: client, 139 url: cfg.URL, 140 }, nil 141 } 142 143 // Build Http Client with TLS 144 func buildTLSHTTPClient(config *auth.TLS) (*http.Client, error) { 145 tlsConfig, err := auth.NewTLSConfig(config) 146 if err != nil { 147 return nil, err 148 } 149 150 transport := &http.Transport{TLSClientConfig: tlsConfig} 151 tlsClient := &http.Client{Transport: transport} 152 153 return tlsClient, nil 154 } 155 156 func (c *clientImpl) Get(ctx context.Context, index string, docID string) (*elastic.GetResult, error) { 157 return c.esClient.Get().Index(index).Id(docID).Do(ctx) 158 } 159 160 func (c *clientImpl) Search(ctx context.Context, p *SearchParameters) (*elastic.SearchResult, error) { 161 searchSource := elastic.NewSearchSource(). 162 Query(p.Query). 163 SortBy(p.Sorter...) 164 165 if p.PointInTime != nil { 166 searchSource.PointInTime(p.PointInTime) 167 } 168 169 if p.PageSize != 0 { 170 searchSource.Size(p.PageSize) 171 } 172 173 if len(p.SearchAfter) != 0 { 174 searchSource.SearchAfter(p.SearchAfter...) 175 } 176 177 searchService := c.esClient.Search().SearchSource(searchSource) 178 // If pit is specified, index must not be used. 179 if p.PointInTime == nil { 180 searchService.Index(p.Index) 181 } 182 183 return searchService.Do(ctx) 184 } 185 186 func (c *clientImpl) OpenScroll( 187 ctx context.Context, 188 p *SearchParameters, 189 keepAliveInterval string, 190 ) (*elastic.SearchResult, error) { 191 scrollService := elastic.NewScrollService(c.esClient). 192 Index(p.Index). 193 Query(p.Query). 194 SortBy(p.Sorter...). 195 KeepAlive(keepAliveInterval) 196 if p.PageSize != 0 { 197 scrollService.Size(p.PageSize) 198 } 199 return scrollService.Do(ctx) 200 } 201 202 func (c *clientImpl) Scroll( 203 ctx context.Context, 204 id string, 205 keepAliveInterval string, 206 ) (*elastic.SearchResult, error) { 207 return elastic.NewScrollService(c.esClient).ScrollId(id).KeepAlive(keepAliveInterval).Do(ctx) 208 } 209 210 func (c *clientImpl) CloseScroll(ctx context.Context, id string) error { 211 return elastic.NewScrollService(c.esClient).ScrollId(id).Clear(ctx) 212 } 213 214 func (c *clientImpl) IsPointInTimeSupported(ctx context.Context) bool { 215 c.initIsPointInTimeSupported.Do(func() { 216 c.isPointInTimeSupported = c.queryPointInTimeSupported(ctx) 217 }) 218 return c.isPointInTimeSupported 219 } 220 221 func (c *clientImpl) queryPointInTimeSupported(ctx context.Context) bool { 222 result, _, err := c.esClient.Ping(c.url.String()).Do(ctx) 223 if err != nil { 224 return false 225 } 226 if result == nil || result.Version.BuildFlavor != pointInTimeSupportedFlavor { 227 return false 228 } 229 esVersion, err := semver.ParseTolerant(result.Version.Number) 230 if err != nil { 231 return false 232 } 233 return pointInTimeSupportedIn(esVersion) 234 } 235 236 func (c *clientImpl) OpenPointInTime(ctx context.Context, index string, keepAliveInterval string) (string, error) { 237 resp, err := c.esClient.OpenPointInTime(index).KeepAlive(keepAliveInterval).Do(ctx) 238 if err != nil { 239 return "", err 240 } 241 return resp.Id, nil 242 } 243 244 func (c *clientImpl) ClosePointInTime(ctx context.Context, id string) (bool, error) { 245 resp, err := c.esClient.ClosePointInTime(id).Do(ctx) 246 if err != nil { 247 return false, err 248 } 249 return resp.Succeeded, nil 250 } 251 252 func (c *clientImpl) Count(ctx context.Context, index string, query elastic.Query) (int64, error) { 253 return c.esClient.Count(index).Query(query).Do(ctx) 254 } 255 256 func (c *clientImpl) CountGroupBy( 257 ctx context.Context, 258 index string, 259 query elastic.Query, 260 aggName string, 261 agg elastic.Aggregation, 262 ) (*elastic.SearchResult, error) { 263 searchSource := elastic.NewSearchSource(). 264 Query(query). 265 Size(0). 266 Aggregation(aggName, agg) 267 return c.esClient.Search(index).SearchSource(searchSource).Do(ctx) 268 } 269 270 func (c *clientImpl) RunBulkProcessor(ctx context.Context, p *BulkProcessorParameters) (BulkProcessor, error) { 271 esBulkProcessor, err := c.esClient.BulkProcessor(). 272 Name(p.Name). 273 Workers(p.NumOfWorkers). 274 BulkActions(p.BulkActions). 275 BulkSize(p.BulkSize). 276 FlushInterval(p.FlushInterval). 277 Before(p.BeforeFunc). 278 After(p.AfterFunc). 279 // Disable built-in retry logic because visibility task processor has its own. 280 RetryItemStatusCodes(). 281 Do(ctx) 282 283 return newBulkProcessor(esBulkProcessor), err 284 } 285 286 func (c *clientImpl) PutMapping(ctx context.Context, index string, mapping map[string]enumspb.IndexedValueType) (bool, error) { 287 body := buildMappingBody(mapping) 288 resp, err := c.esClient.PutMapping().Index(index).BodyJson(body).Do(ctx) 289 if err != nil { 290 return false, err 291 } 292 return resp.Acknowledged, err 293 } 294 295 func (c *clientImpl) WaitForYellowStatus(ctx context.Context, index string) (string, error) { 296 resp, err := c.esClient.ClusterHealth().Index(index).WaitForYellowStatus().Do(ctx) 297 if err != nil { 298 return "", err 299 } 300 return resp.Status, err 301 } 302 303 func (c *clientImpl) GetMapping(ctx context.Context, index string) (map[string]string, error) { 304 // Manually build mapping request because olivere/elastic/v7 client doesn't work with ES8 305 path, err := uritemplates.Expand("/{index}/_mapping", map[string]string{ 306 "index": index, 307 }) 308 if err != nil { 309 return nil, err 310 } 311 312 // Get HTTP response 313 res, err := c.esClient.PerformRequest(ctx, elastic.PerformRequestOptions{ 314 Method: "GET", 315 Path: path, 316 Params: url.Values{}, 317 Headers: http.Header{}, 318 }) 319 if err != nil { 320 return nil, err 321 } 322 323 // Decode body 324 var body map[string]interface{} 325 if err := json.Unmarshal(res.Body, &body); err != nil { 326 return nil, err 327 } 328 329 return convertMappingBody(body, index), nil 330 } 331 332 func (c *clientImpl) GetDateFieldType() string { 333 return "date_nanos" 334 } 335 336 func (c *clientImpl) CreateIndex(ctx context.Context, index string) (bool, error) { 337 resp, err := c.esClient.CreateIndex(index).Do(ctx) 338 if err != nil { 339 return false, err 340 } 341 return resp.Acknowledged, nil 342 } 343 344 func (c *clientImpl) IsNotFoundError(err error) bool { 345 return elastic.IsNotFound(err) 346 } 347 348 func (c *clientImpl) CatIndices(ctx context.Context) (elastic.CatIndicesResponse, error) { 349 return c.esClient.CatIndices().Do(ctx) 350 } 351 352 func (c *clientImpl) Bulk() BulkService { 353 return newBulkService(c.esClient.Bulk()) 354 } 355 356 func (c *clientImpl) IndexPutTemplate(ctx context.Context, templateName string, bodyString string) (bool, error) { 357 //lint:ignore SA1019 Changing to IndexPutIndexTemplate requires template changes and will be done separately. 358 resp, err := c.esClient.IndexPutTemplate(templateName).BodyString(bodyString).Do(ctx) 359 if err != nil { 360 return false, err 361 } 362 return resp.Acknowledged, nil 363 } 364 365 func (c *clientImpl) IndexExists(ctx context.Context, indexName string) (bool, error) { 366 return c.esClient.IndexExists(indexName).Do(ctx) 367 } 368 369 func (c *clientImpl) DeleteIndex(ctx context.Context, indexName string) (bool, error) { 370 resp, err := c.esClient.DeleteIndex(indexName).Do(ctx) 371 if err != nil { 372 return false, err 373 } 374 return resp.Acknowledged, nil 375 } 376 377 func (c *clientImpl) IndexPutSettings(ctx context.Context, indexName string, bodyString string) (bool, error) { 378 resp, err := c.esClient.IndexPutSettings(indexName).BodyString(bodyString).Do(ctx) 379 if err != nil { 380 return false, err 381 } 382 return resp.Acknowledged, nil 383 } 384 385 func (c *clientImpl) IndexGetSettings(ctx context.Context, indexName string) (map[string]*elastic.IndicesGetSettingsResponse, error) { 386 return c.esClient.IndexGetSettings(indexName).Do(ctx) 387 } 388 389 func (c *clientImpl) Delete(ctx context.Context, indexName string, docID string, version int64) error { 390 _, err := c.esClient.Delete(). 391 Index(indexName). 392 Id(docID). 393 Version(version). 394 VersionType(versionTypeExternal). 395 Do(ctx) 396 return err 397 } 398 399 // Ping returns whether or not the elastic search cluster is available 400 func (c *clientImpl) Ping(ctx context.Context) error { 401 _, _, err := c.esClient.Ping(c.url.String()).Do(ctx) 402 return err 403 } 404 405 func getLoggerOptions(logLevel string, logger log.Logger) []elastic.ClientOptionFunc { 406 switch { 407 case strings.EqualFold(logLevel, "trace"): 408 return []elastic.ClientOptionFunc{ 409 elastic.SetErrorLog(newErrorLogger(logger)), 410 elastic.SetInfoLog(newInfoLogger(logger)), 411 elastic.SetTraceLog(newInfoLogger(logger)), 412 } 413 case strings.EqualFold(logLevel, "info"): 414 return []elastic.ClientOptionFunc{ 415 elastic.SetErrorLog(newErrorLogger(logger)), 416 elastic.SetInfoLog(newInfoLogger(logger)), 417 } 418 case strings.EqualFold(logLevel, "error"), logLevel == "": // Default is to log errors only. 419 return []elastic.ClientOptionFunc{ 420 elastic.SetErrorLog(newErrorLogger(logger)), 421 } 422 default: 423 return nil 424 } 425 } 426 427 func buildMappingBody(mapping map[string]enumspb.IndexedValueType) map[string]interface{} { 428 properties := make(map[string]interface{}, len(mapping)) 429 for fieldName, fieldType := range mapping { 430 var typeMap map[string]interface{} 431 switch fieldType { 432 case enumspb.INDEXED_VALUE_TYPE_TEXT: 433 typeMap = map[string]interface{}{"type": "text"} 434 case enumspb.INDEXED_VALUE_TYPE_KEYWORD, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST: 435 typeMap = map[string]interface{}{"type": "keyword"} 436 case enumspb.INDEXED_VALUE_TYPE_INT: 437 typeMap = map[string]interface{}{"type": "long"} 438 case enumspb.INDEXED_VALUE_TYPE_DOUBLE: 439 typeMap = map[string]interface{}{ 440 "type": "scaled_float", 441 "scaling_factor": 10000, 442 } 443 case enumspb.INDEXED_VALUE_TYPE_BOOL: 444 typeMap = map[string]interface{}{"type": "boolean"} 445 case enumspb.INDEXED_VALUE_TYPE_DATETIME: 446 typeMap = map[string]interface{}{"type": "date_nanos"} 447 } 448 if typeMap != nil { 449 properties[fieldName] = typeMap 450 } 451 } 452 453 body := map[string]interface{}{ 454 "properties": properties, 455 } 456 return body 457 } 458 459 func convertMappingBody(esMapping map[string]interface{}, indexName string) map[string]string { 460 result := make(map[string]string) 461 index, ok := esMapping[indexName] 462 if !ok { 463 return result 464 } 465 indexMap, ok := index.(map[string]interface{}) 466 if !ok { 467 return result 468 } 469 mappings, ok := indexMap["mappings"] 470 if !ok { 471 return result 472 } 473 mappingsMap, ok := mappings.(map[string]interface{}) 474 if !ok { 475 return result 476 } 477 478 properties, ok := mappingsMap["properties"] 479 if !ok { 480 return result 481 } 482 propMap, ok := properties.(map[string]interface{}) 483 if !ok { 484 return result 485 } 486 487 for fieldName, fieldProp := range propMap { 488 fieldPropMap, ok := fieldProp.(map[string]interface{}) 489 if !ok { 490 continue 491 } 492 tYpe, ok := fieldPropMap["type"] 493 if !ok { 494 continue 495 } 496 typeStr, ok := tYpe.(string) 497 if !ok { 498 continue 499 } 500 result[fieldName] = typeStr 501 } 502 503 return result 504 }