diff --git a/service/frontend/namespace_handler.go b/service/frontend/namespace_handler.go index b5a929ff14..a4a82c1718 100644 --- a/service/frontend/namespace_handler.go +++ b/service/frontend/namespace_handler.go @@ -4,6 +4,7 @@ package frontend import ( "context" + "fmt" "time" "github.com/google/uuid" @@ -946,10 +947,18 @@ func (d *namespaceHandler) upsertCustomSearchAttributesAliases( upsert map[string]string, ) (map[string]string, error) { result := util.CloneMapNonNil(current) + currentAliasToField := util.InverseMap(current) for key, value := range upsert { if value == "" { delete(result, key) } else if _, ok := current[key]; !ok { + if _, aliasExists := currentAliasToField[value]; aliasExists { + d.logger.Warn( + fmt.Sprintf(errSearchAttributeAlreadyExistsMessage, value), + tag.String("visibility-search-attribute", value), + ) + continue + } result[key] = value } else { return nil, errCustomSearchAttributeFieldAlreadyAllocated diff --git a/service/frontend/namespace_handler_test.go b/service/frontend/namespace_handler_test.go index 590449dadd..5a3aacedd5 100644 --- a/service/frontend/namespace_handler_test.go +++ b/service/frontend/namespace_handler_test.go @@ -1955,3 +1955,58 @@ func (s *namespaceHandlerCommonSuite) TestWorkflowRuleEviction() { func (s *namespaceHandlerCommonSuite) getRandomNamespace() string { return "namespace" + uuid.NewString() } + +func (s *namespaceHandlerCommonSuite) TestUpsertCustomSearchAttributesAliases() { + tests := []struct { + name string + current map[string]string + upsert map[string]string + expected map[string]string + expectedErr error + }{ + { + name: "add to empty", + current: map[string]string{}, + upsert: map[string]string{"Keyword01": "MyAttr"}, + expected: map[string]string{"Keyword01": "MyAttr"}, + }, + { + name: "add to existing", + current: map[string]string{"Keyword01": "MyAttr"}, + upsert: map[string]string{"Keyword02": "OtherAttr"}, + expected: map[string]string{"Keyword01": "MyAttr", "Keyword02": "OtherAttr"}, + }, + { + name: "field already allocated", + current: map[string]string{"Keyword01": "MyAttr"}, + upsert: map[string]string{"Keyword01": "NewAttr"}, + expectedErr: errCustomSearchAttributeFieldAlreadyAllocated, + }, + { + // Race condition: a concurrent request already claimed the same alias for a + // different field. The alias already exists so we skip it — idempotent success. + name: "alias already in use by a different field is skipped", + current: map[string]string{"Keyword01": "RaceTestAttr"}, + upsert: map[string]string{"Keyword03": "RaceTestAttr"}, + expected: map[string]string{"Keyword01": "RaceTestAttr"}, + }, + { + name: "delete existing", + current: map[string]string{"Keyword01": "MyAttr"}, + upsert: map[string]string{"Keyword01": ""}, + expected: map[string]string{}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + result, err := s.handler.upsertCustomSearchAttributesAliases(tc.current, tc.upsert) + if tc.expectedErr != nil { + s.Equal(tc.expectedErr, err) + } else { + s.Require().NoError(err) + s.Equal(tc.expected, result) + } + }) + } +}