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 }