Skip to content

Commit 68f8c22

Browse files
enable namespace mapper with fallbacks (#9664)
## What changed? enable namespace mapper with fallbacks to the cluster metadata. ## Why? Allow backwards compatible rollout of search attribute mapper unification for ElasticSearch backed visibility stores. ## How did you test it? - [X] built - [X] run locally and tested manually - [X] covered by existing tests - [X] added new unit test(s) - [ ] added new functional test(s)
1 parent 2993285 commit 68f8c22

11 files changed

Lines changed: 334 additions & 114 deletions

File tree

common/persistence/visibility/store/query/converter_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ const (
2323
testNamespaceID = namespace.ID("test-namespace-id")
2424
)
2525

26+
type identityMapper struct{}
27+
28+
func (identityMapper) GetAlias(fieldName string, _ string) (string, error) {
29+
return fieldName, nil
30+
}
31+
32+
func (identityMapper) GetFieldName(alias string, _ string) (string, error) {
33+
return alias, nil
34+
}
35+
2636
func TestWithSearchAttributeInterceptor(t *testing.T) {
2737
t.Parallel()
2838
r := require.New(t)
@@ -1917,7 +1927,7 @@ func TestQueryConverter_ResolveSearchAttributeAlias(t *testing.T) {
19171927
)
19181928

19191929
if tc.useNoopMapper {
1920-
queryConverter.saMapper = searchattribute.NewNoopMapper()
1930+
queryConverter.saMapper = identityMapper{}
19211931
}
19221932

19231933
fn, ft, err := queryConverter.resolveSearchAttributeAlias(tc.in)

common/resource/fx.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,12 @@ func SearchAttributeMapperProviderProvider(
156156
searchAttributeProvider searchattribute.Provider,
157157
persistenceConfig *config.Persistence,
158158
) searchattribute.MapperProvider {
159+
primaryVisibilityStoreConfig := persistenceConfig.GetVisibilityStoreConfig()
159160
return searchattribute.NewMapperProvider(
160161
saMapper,
161162
namespaceRegistry,
162163
searchAttributeProvider,
163-
persistenceConfig.IsSQLVisibilityStore() || persistenceConfig.IsCustomVisibilityStore(),
164+
primaryVisibilityStoreConfig.GetIndexName(),
164165
)
165166
}
166167

common/searchattribute/mapper.go

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ package searchattribute
44

55
import (
66
"errors"
7+
"fmt"
78

89
commonpb "go.temporal.io/api/common/v1"
10+
enumspb "go.temporal.io/api/enums/v1"
911
"go.temporal.io/api/serviceerror"
1012
"go.temporal.io/server/common/namespace"
1113
"go.temporal.io/server/common/searchattribute/sadefs"
@@ -20,101 +22,106 @@ type (
2022
GetFieldName(alias string, namespace string) (string, error)
2123
}
2224

23-
noopMapper struct{}
24-
25-
// This mapper is to be backwards compatible with versions before v1.20.
26-
// Users using standard visibility might have registered custom search attributes.
27-
// Those search attributes won't be searchable, as they weren't before version v1.20.
28-
// Thus, this mapper will allow those search attributes to be used without being alised.
29-
backCompMapper_v1_20 struct {
30-
mapper Mapper
31-
emptyStringNameTypeMap NameTypeMap
25+
// This mapper preserves legacy custom search attribute behavior by falling back
26+
// to identity mapping when the wrapped mapper misses but cluster metadata still
27+
// recognizes the name as a legacy custom search attribute.
28+
backCompMapper struct {
29+
mapper Mapper
30+
fallbackNameTypeMap NameTypeMap
3231
}
3332

3433
MapperProvider interface {
3534
GetMapper(nsName namespace.Name) (Mapper, error)
3635
}
3736

3837
mapperProviderImpl struct {
39-
customMapper Mapper
40-
namespaceRegistry namespace.Registry
41-
searchAttributesProvider Provider
42-
enableMapperFromNamespace bool
38+
customMapper Mapper
39+
namespaceRegistry namespace.Registry
40+
searchAttributesProvider Provider
41+
fallbackIndexName string
4342
}
4443
)
4544

46-
var _ Mapper = (*noopMapper)(nil)
47-
var _ Mapper = (*backCompMapper_v1_20)(nil)
45+
var _ Mapper = (*backCompMapper)(nil)
4846
var _ Mapper = (*namespace.CustomSearchAttributesMapper)(nil)
4947
var _ MapperProvider = (*mapperProviderImpl)(nil)
5048

51-
func (m *noopMapper) GetAlias(fieldName string, _ string) (string, error) {
52-
return fieldName, nil
53-
}
54-
55-
func (m *noopMapper) GetFieldName(alias string, _ string) (string, error) {
56-
return alias, nil
57-
}
58-
59-
func (m *backCompMapper_v1_20) GetAlias(fieldName string, namespaceName string) (string, error) {
49+
func (m *backCompMapper) GetAlias(fieldName string, namespaceName string) (string, error) {
6050
alias, firstErr := m.mapper.GetAlias(fieldName, namespaceName)
6151
if firstErr != nil {
62-
_, err := m.emptyStringNameTypeMap.getType(fieldName, customCategory)
63-
if err != nil {
52+
if !m.isLegacyCustomSearchAttribute(fieldName) {
6453
return "", firstErr
6554
}
66-
// this is custom search attribute registered in pre-v1.20
55+
// this is a custom search attribute registered through cluster metadata.
6756
return fieldName, nil
6857
}
6958
return alias, nil
7059
}
7160

72-
func (m *backCompMapper_v1_20) GetFieldName(alias string, namespaceName string) (string, error) {
61+
func (m *backCompMapper) GetFieldName(alias string, namespaceName string) (string, error) {
7362
fieldName, firstErr := m.mapper.GetFieldName(alias, namespaceName)
7463
if firstErr != nil {
75-
_, err := m.emptyStringNameTypeMap.getType(alias, customCategory)
76-
if err != nil {
64+
if !m.isLegacyCustomSearchAttribute(alias) {
7765
return "", firstErr
7866
}
79-
// this is custom search attribute registered in pre-v1.20
67+
// this is a custom search attribute registered through cluster metadata.
8068
return alias, nil
8169
}
8270
return fieldName, nil
8371
}
8472

73+
func (m *backCompMapper) isLegacyCustomSearchAttribute(name string) bool {
74+
_, err := m.fallbackNameTypeMap.getType(name, customCategory)
75+
return err == nil
76+
}
77+
8578
func NewMapperProvider(
8679
customMapper Mapper,
8780
namespaceRegistry namespace.Registry,
8881
searchAttributesProvider Provider,
89-
enableMapperFromNamespace bool,
82+
fallbackIndexName string,
9083
) MapperProvider {
9184
return &mapperProviderImpl{
92-
customMapper: customMapper,
93-
namespaceRegistry: namespaceRegistry,
94-
searchAttributesProvider: searchAttributesProvider,
95-
enableMapperFromNamespace: enableMapperFromNamespace,
85+
customMapper: customMapper,
86+
namespaceRegistry: namespaceRegistry,
87+
searchAttributesProvider: searchAttributesProvider,
88+
fallbackIndexName: fallbackIndexName,
9689
}
9790
}
9891

9992
func (m *mapperProviderImpl) GetMapper(nsName namespace.Name) (Mapper, error) {
10093
if m.customMapper != nil {
10194
return m.customMapper, nil
10295
}
103-
if !m.enableMapperFromNamespace {
104-
return &noopMapper{}, nil
105-
}
10696
saMapper, err := m.namespaceRegistry.GetCustomSearchAttributesMapper(nsName)
10797
if err != nil {
10898
return nil, err
10999
}
110-
// if there's an error, it returns an empty object, which is expected here
111-
emptyStringNameTypeMap, _ := m.searchAttributesProvider.GetSearchAttributes("", false)
112-
return &backCompMapper_v1_20{
113-
mapper: &saMapper,
114-
emptyStringNameTypeMap: emptyStringNameTypeMap,
100+
fallbackNameTypeMap := NameTypeMap{}
101+
if m.fallbackIndexName != "" {
102+
nameTypeMap, err := m.searchAttributesProvider.GetSearchAttributes(m.fallbackIndexName, false)
103+
if err != nil {
104+
return nil, fmt.Errorf("failed to load search attributes for fallback index %q: %w", m.fallbackIndexName, err)
105+
}
106+
fallbackNameTypeMap = legacyCustomSearchAttributes(nameTypeMap)
107+
}
108+
return &backCompMapper{
109+
mapper: &saMapper,
110+
fallbackNameTypeMap: fallbackNameTypeMap,
115111
}, nil
116112
}
117113

114+
func legacyCustomSearchAttributes(nameTypeMap NameTypeMap) NameTypeMap {
115+
legacyCustomSearchAttributes := make(map[string]enumspb.IndexedValueType)
116+
for name, valueType := range nameTypeMap.Custom() {
117+
if sadefs.IsPreallocatedCSAFieldName(name, valueType) {
118+
continue
119+
}
120+
legacyCustomSearchAttributes[name] = valueType
121+
}
122+
return NewNameTypeMap(legacyCustomSearchAttributes)
123+
}
124+
118125
// AliasFields returns SearchAttributes struct where each custom search attribute name is replaced with alias.
119126
// If no replacement where made, it returns nil which means that original SearchAttributes struct should be used.
120127
func AliasFields(

common/searchattribute/mapper_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package searchattribute
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/stretchr/testify/require"
78
commonpb "go.temporal.io/api/common/v1"
9+
enumspb "go.temporal.io/api/enums/v1"
810
"go.temporal.io/api/serviceerror"
11+
"go.temporal.io/server/common/namespace"
12+
"go.uber.org/mock/gomock"
913
)
1014

1115
func Test_AliasFields(t *testing.T) {
@@ -137,3 +141,115 @@ func Test_UnaliasFields(t *testing.T) {
137141
require.NoError(t, err)
138142
require.Equal(t, sa, sb, "when there is nothin to unalias should return received attributes")
139143
}
144+
145+
type staticSearchAttributesProvider struct {
146+
nameTypeMaps map[string]NameTypeMap
147+
err error
148+
}
149+
150+
func (s staticSearchAttributesProvider) GetSearchAttributes(indexName string, _ bool) (NameTypeMap, error) {
151+
if s.err != nil {
152+
return NameTypeMap{}, s.err
153+
}
154+
if nameTypeMap, ok := s.nameTypeMaps[indexName]; ok {
155+
return nameTypeMap, nil
156+
}
157+
return NameTypeMap{}, nil
158+
}
159+
160+
func Test_BackCompMapperFallsBackToClusterMetadataFields(t *testing.T) {
161+
mapper := &backCompMapper{
162+
mapper: &TestMapper{},
163+
fallbackNameTypeMap: TestNameTypeMap(),
164+
}
165+
166+
alias, err := mapper.GetAlias("Keyword02", "error-namespace")
167+
require.NoError(t, err)
168+
require.Equal(t, "Keyword02", alias)
169+
170+
fieldName, err := mapper.GetFieldName("Keyword02", "error-namespace")
171+
require.NoError(t, err)
172+
require.Equal(t, "Keyword02", fieldName)
173+
}
174+
175+
func TestMapperProviderUsesConfiguredVisibilityIndexForBackCompatFallback(t *testing.T) {
176+
controller := gomock.NewController(t)
177+
nsRegistry := namespace.NewMockRegistry(controller)
178+
nsRegistry.EXPECT().
179+
GetCustomSearchAttributesMapper(namespace.Name("test-namespace")).
180+
Return(namespace.CustomSearchAttributesMapper{}, nil)
181+
182+
mapperProvider := NewMapperProvider(
183+
nil,
184+
nsRegistry,
185+
staticSearchAttributesProvider{
186+
nameTypeMaps: map[string]NameTypeMap{
187+
"test-visibility-index": NewNameTypeMap(map[string]enumspb.IndexedValueType{
188+
"LegacyKeyword": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
189+
}),
190+
},
191+
},
192+
"test-visibility-index",
193+
)
194+
195+
mapper, err := mapperProvider.GetMapper(namespace.Name("test-namespace"))
196+
require.NoError(t, err)
197+
198+
alias, err := mapper.GetAlias("LegacyKeyword", "error-namespace")
199+
require.NoError(t, err)
200+
require.Equal(t, "LegacyKeyword", alias)
201+
202+
fieldName, err := mapper.GetFieldName("LegacyKeyword", "error-namespace")
203+
require.NoError(t, err)
204+
require.Equal(t, "LegacyKeyword", fieldName)
205+
}
206+
207+
func TestMapperProviderDoesNotTreatPreallocatedFieldsAsLegacyCustomAttributes(t *testing.T) {
208+
controller := gomock.NewController(t)
209+
nsRegistry := namespace.NewMockRegistry(controller)
210+
nsRegistry.EXPECT().
211+
GetCustomSearchAttributesMapper(namespace.Name("test-namespace")).
212+
Return(namespace.CustomSearchAttributesMapper{}, nil)
213+
214+
mapperProvider := NewMapperProvider(
215+
nil,
216+
nsRegistry,
217+
staticSearchAttributesProvider{
218+
nameTypeMaps: map[string]NameTypeMap{
219+
"test-visibility-index": TestNameTypeMap(),
220+
},
221+
},
222+
"test-visibility-index",
223+
)
224+
225+
mapper, err := mapperProvider.GetMapper(namespace.Name("test-namespace"))
226+
require.NoError(t, err)
227+
228+
_, err = mapper.GetFieldName("Text01", "error-namespace")
229+
require.Error(t, err)
230+
var invalidArgumentErr *serviceerror.InvalidArgument
231+
require.ErrorAs(t, err, &invalidArgumentErr)
232+
}
233+
234+
func TestMapperProviderReturnsFallbackLookupError(t *testing.T) {
235+
controller := gomock.NewController(t)
236+
nsRegistry := namespace.NewMockRegistry(controller)
237+
nsRegistry.EXPECT().
238+
GetCustomSearchAttributesMapper(namespace.Name("test-namespace")).
239+
Return(namespace.CustomSearchAttributesMapper{}, nil)
240+
241+
expectedErr := errors.New("boom")
242+
mapperProvider := NewMapperProvider(
243+
nil,
244+
nsRegistry,
245+
staticSearchAttributesProvider{
246+
err: expectedErr,
247+
},
248+
"test-visibility-index",
249+
)
250+
251+
_, err := mapperProvider.GetMapper(namespace.Name("test-namespace"))
252+
require.Error(t, err)
253+
require.ErrorContains(t, err, `failed to load search attributes for fallback index "test-visibility-index"`)
254+
require.ErrorContains(t, err, expectedErr.Error())
255+
}

common/searchattribute/test_provider.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
enumspb "go.temporal.io/api/enums/v1"
99
"go.temporal.io/api/serviceerror"
10+
"go.temporal.io/server/common/namespace"
1011
"go.temporal.io/server/common/searchattribute/sadefs"
1112
)
1213

@@ -15,6 +16,10 @@ type (
1516
es bool
1617
}
1718

19+
testMapperProvider struct {
20+
mapper Mapper
21+
}
22+
1823
TestMapper struct {
1924
Namespace string
2025
WithCustomScheduleID bool
@@ -23,6 +28,7 @@ type (
2328

2429
var _ Provider = (*TestProvider)(nil)
2530
var _ Mapper = (*TestMapper)(nil)
31+
var _ MapperProvider = (*testMapperProvider)(nil)
2632

2733
var (
2834
esCustomSearchAttributes = map[string]enumspb.IndexedValueType{
@@ -61,6 +67,10 @@ func TestEsNameTypeMap() NameTypeMap {
6167
return NewNameTypeMap(csa)
6268
}
6369

70+
func TestSearchAttributesToRegister() map[string]enumspb.IndexedValueType {
71+
return maps.Clone(esCustomSearchAttributes)
72+
}
73+
6474
func TestEsNameTypeMapWithScheduleID() NameTypeMap {
6575
res := TestEsNameTypeMap()
6676
res.customSearchAttributes[sadefs.ScheduleID] = enumspb.INDEXED_VALUE_TYPE_KEYWORD
@@ -134,12 +144,12 @@ func (t *TestMapper) GetFieldName(alias string, namespace string) (string, error
134144
return "", serviceerror.NewInvalidArgument("unknown namespace")
135145
}
136146

137-
func NewNoopMapper() Mapper {
138-
return &noopMapper{}
147+
func NewTestMapperProvider(customMapper Mapper) MapperProvider {
148+
return &testMapperProvider{mapper: customMapper}
139149
}
140150

141-
func NewTestMapperProvider(customMapper Mapper) MapperProvider {
142-
return NewMapperProvider(customMapper, nil, NewTestProvider(), false)
151+
func (p *testMapperProvider) GetMapper(namespace.Name) (Mapper, error) {
152+
return p.mapper, nil
143153
}
144154

145155
func NewNameTypeMapStub(attributes map[string]enumspb.IndexedValueType) NameTypeMap {

temporal/fx.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,9 +664,7 @@ func ApplyClusterMetadataConfigProvider(
664664
}
665665
indexSearchAttributes := make(map[string]*persistencespb.IndexSearchAttributes)
666666
for _, ds := range visDataStores {
667-
if ds.SQL != nil || ds.CustomDataStoreConfig != nil {
668-
indexSearchAttributes[ds.GetIndexName()] = sadefs.GetDBIndexSearchAttributes(visCSAOverride)
669-
}
667+
indexSearchAttributes[ds.GetIndexName()] = sadefs.GetDBIndexSearchAttributes(visCSAOverride)
670668
}
671669

672670
clusterMetadata := svc.ClusterMetadata

0 commit comments

Comments
 (0)