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  }