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

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package searchattribute
    26  
    27  import (
    28  	"context"
    29  	"sync"
    30  	"sync/atomic"
    31  	"time"
    32  
    33  	enumspb "go.temporal.io/api/enums/v1"
    34  	"go.temporal.io/api/serviceerror"
    35  	"golang.org/x/exp/maps"
    36  
    37  	persistencespb "go.temporal.io/server/api/persistence/v1"
    38  	"go.temporal.io/server/common/clock"
    39  	"go.temporal.io/server/common/dynamicconfig"
    40  	"go.temporal.io/server/common/headers"
    41  	"go.temporal.io/server/common/persistence"
    42  )
    43  
    44  const (
    45  	cacheRefreshInterval              = 60 * time.Second
    46  	cacheRefreshIfUnavailableInterval = 20 * time.Second
    47  )
    48  
    49  type (
    50  	managerImpl struct {
    51  		timeSource             clock.TimeSource
    52  		clusterMetadataManager persistence.ClusterMetadataManager
    53  		forceRefresh           dynamicconfig.BoolPropertyFn
    54  
    55  		cacheUpdateMutex sync.Mutex
    56  		cache            atomic.Value // of type cache
    57  	}
    58  
    59  	cache struct {
    60  		// indexName -> NameTypeMap
    61  		searchAttributes map[string]NameTypeMap
    62  		dbVersion        int64
    63  		expireOn         time.Time
    64  	}
    65  )
    66  
    67  var _ Manager = (*managerImpl)(nil)
    68  
    69  func NewManager(
    70  	timeSource clock.TimeSource,
    71  	clusterMetadataManager persistence.ClusterMetadataManager,
    72  	forceRefresh dynamicconfig.BoolPropertyFn,
    73  ) *managerImpl {
    74  
    75  	var saCache atomic.Value
    76  	saCache.Store(cache{
    77  		searchAttributes: map[string]NameTypeMap{},
    78  		dbVersion:        0,
    79  		expireOn:         time.Time{},
    80  	})
    81  
    82  	return &managerImpl{
    83  		timeSource:             timeSource,
    84  		cache:                  saCache,
    85  		clusterMetadataManager: clusterMetadataManager,
    86  		forceRefresh:           forceRefresh,
    87  	}
    88  }
    89  
    90  // GetSearchAttributes returns all search attributes (including system and build-in) for specified index.
    91  // indexName can be an empty string for backward compatibility.
    92  func (m *managerImpl) GetSearchAttributes(
    93  	indexName string,
    94  	forceRefreshCache bool,
    95  ) (NameTypeMap, error) {
    96  
    97  	now := m.timeSource.Now()
    98  	saCache := m.cache.Load().(cache)
    99  
   100  	if m.needRefreshCache(saCache, forceRefreshCache, now) {
   101  		m.cacheUpdateMutex.Lock()
   102  		saCache = m.cache.Load().(cache)
   103  		if m.needRefreshCache(saCache, forceRefreshCache, now) {
   104  			var err error
   105  			saCache, err = m.refreshCache(saCache, now)
   106  			if err != nil {
   107  				m.cacheUpdateMutex.Unlock()
   108  				return NameTypeMap{}, err
   109  			}
   110  		}
   111  		m.cacheUpdateMutex.Unlock()
   112  	}
   113  
   114  	result := NameTypeMap{}
   115  	indexSearchAttributes, ok := saCache.searchAttributes[indexName]
   116  	if ok {
   117  		result.customSearchAttributes = maps.Clone(indexSearchAttributes.customSearchAttributes)
   118  	}
   119  
   120  	// TODO (rodrigozhou): remove following block for v1.21.
   121  	// Try to look for the empty string indexName for backward compatibility: up to v1.19,
   122  	// empty string was used when Elasticsearch was not configured.
   123  	// If there's a value, merging with current index name value. This is to avoid handling
   124  	// all code references to GetSearchAttributes.
   125  	if indexName != "" {
   126  		indexSearchAttributes, ok = saCache.searchAttributes[""]
   127  		if ok {
   128  			if result.customSearchAttributes == nil {
   129  				result.customSearchAttributes = maps.Clone(indexSearchAttributes.customSearchAttributes)
   130  			} else {
   131  				maps.Copy(result.customSearchAttributes, indexSearchAttributes.customSearchAttributes)
   132  			}
   133  		}
   134  	}
   135  	return result, nil
   136  }
   137  
   138  func (m *managerImpl) needRefreshCache(saCache cache, forceRefreshCache bool, now time.Time) bool {
   139  	return forceRefreshCache || saCache.expireOn.Before(now) || m.forceRefresh()
   140  }
   141  
   142  func (m *managerImpl) refreshCache(saCache cache, now time.Time) (cache, error) {
   143  	// TODO: specify a timeout for the context
   144  	ctx := headers.SetCallerInfo(
   145  		context.TODO(),
   146  		headers.SystemBackgroundCallerInfo,
   147  	)
   148  
   149  	clusterMetadata, err := m.clusterMetadataManager.GetCurrentClusterMetadata(ctx)
   150  	if err != nil {
   151  		switch err.(type) {
   152  		case *serviceerror.NotFound:
   153  			// NotFound means cluster metadata was never persisted and custom search attributes are not defined.
   154  			// Ignore the error.
   155  			saCache.expireOn = now.Add(cacheRefreshInterval)
   156  		case *serviceerror.Unavailable:
   157  			// If persistence is Unavailable, ignore the error and use existing cache for cacheRefreshIfUnavailableInterval.
   158  			saCache.expireOn = now.Add(cacheRefreshIfUnavailableInterval)
   159  		default:
   160  			return saCache, err
   161  		}
   162  		m.cache.Store(saCache)
   163  		return saCache, nil
   164  	}
   165  
   166  	// clusterMetadata.Version <= saCache.dbVersion means DB is not changed.
   167  	if clusterMetadata.Version <= saCache.dbVersion {
   168  		saCache.expireOn = now.Add(cacheRefreshInterval)
   169  		m.cache.Store(saCache)
   170  		return saCache, nil
   171  	}
   172  
   173  	saCache = cache{
   174  		searchAttributes: buildIndexNameTypeMap(clusterMetadata.GetIndexSearchAttributes()),
   175  		expireOn:         now.Add(cacheRefreshInterval),
   176  		dbVersion:        clusterMetadata.Version,
   177  	}
   178  	m.cache.Store(saCache)
   179  	return saCache, nil
   180  }
   181  
   182  // SaveSearchAttributes saves search attributes to cluster metadata.
   183  // indexName can be an empty string when Elasticsearch is not configured.
   184  func (m *managerImpl) SaveSearchAttributes(
   185  	ctx context.Context,
   186  	indexName string,
   187  	newCustomSearchAttributes map[string]enumspb.IndexedValueType,
   188  ) error {
   189  
   190  	clusterMetadataResponse, err := m.clusterMetadataManager.GetCurrentClusterMetadata(ctx)
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	clusterMetadata := clusterMetadataResponse.ClusterMetadata
   196  	if clusterMetadata.IndexSearchAttributes == nil {
   197  		clusterMetadata.IndexSearchAttributes = map[string]*persistencespb.IndexSearchAttributes{indexName: nil}
   198  	}
   199  	clusterMetadata.IndexSearchAttributes[indexName] = &persistencespb.IndexSearchAttributes{CustomSearchAttributes: newCustomSearchAttributes}
   200  	_, err = m.clusterMetadataManager.SaveClusterMetadata(ctx, &persistence.SaveClusterMetadataRequest{
   201  		ClusterMetadata: clusterMetadata,
   202  		Version:         clusterMetadataResponse.Version,
   203  	})
   204  	// Flush local cache, even if there was an error, which is most likely version mismatch (=stale cache).
   205  	m.cache.Store(cache{})
   206  
   207  	return err
   208  }