Skip to content

Commit 53517fd

Browse files
rodrigozhoudnr
authored andcommitted
Custom search attributes validation per store (#4655)
1 parent 2830f38 commit 53517fd

21 files changed

+275
-18
lines changed

common/persistence/visibility/manager/visibility_manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type (
4747
GetStoreNames() []string
4848
HasStoreName(stName string) bool
4949
GetIndexName() string
50+
ValidateCustomSearchAttributes(searchAttributes map[string]any) (map[string]any, error)
5051

5152
// Write APIs.
5253
RecordWorkflowExecutionStarted(ctx context.Context, request *RecordWorkflowExecutionStartedRequest) error

common/persistence/visibility/manager/visibility_manager_mock.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/persistence/visibility/store/elasticsearch/visibility_store.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ var _ store.VisibilityStore = (*visibilityStore)(nil)
9595
var (
9696
errUnexpectedJSONFieldType = errors.New("unexpected JSON field type")
9797

98+
minTime = time.Unix(0, 0).UTC()
99+
maxTime = time.Unix(0, math.MaxInt64).UTC()
100+
maxStringLength = 32766
101+
98102
// Default sorter uses the sorting order defined in the index template.
99103
// It is indirectly built so buildPaginationQuery can have access to
100104
// the fields names to build the page query from the token.
@@ -164,6 +168,44 @@ func (s *visibilityStore) GetIndexName() string {
164168
return s.index
165169
}
166170

171+
func (s *visibilityStore) ValidateCustomSearchAttributes(
172+
searchAttributes map[string]any,
173+
) (map[string]any, error) {
174+
validatedSearchAttributes := make(map[string]any, len(searchAttributes))
175+
var invalidValueErrs []error
176+
for saName, saValue := range searchAttributes {
177+
var err error
178+
switch value := saValue.(type) {
179+
case time.Time:
180+
err = validateDatetime(value)
181+
case []time.Time:
182+
for _, item := range value {
183+
if err = validateDatetime(item); err != nil {
184+
break
185+
}
186+
}
187+
case string:
188+
err = validateString(value)
189+
case []string:
190+
for _, item := range value {
191+
if err = validateString(item); err != nil {
192+
break
193+
}
194+
}
195+
}
196+
if err != nil {
197+
invalidValueErrs = append(invalidValueErrs, err)
198+
continue
199+
}
200+
validatedSearchAttributes[saName] = saValue
201+
}
202+
var retError error
203+
if len(invalidValueErrs) > 0 {
204+
retError = store.NewVisibilityStoreInvalidValuesError(invalidValueErrs)
205+
}
206+
return validatedSearchAttributes, retError
207+
}
208+
167209
func (s *visibilityStore) RecordWorkflowExecutionStarted(
168210
ctx context.Context,
169211
request *store.InternalRecordWorkflowExecutionStartedRequest,
@@ -941,6 +983,14 @@ func (s *visibilityStore) generateESDoc(request *store.InternalVisibilityRequest
941983
s.metricsHandler.Counter(metrics.ElasticsearchDocumentGenerateFailuresCount.GetMetricName()).Record(1)
942984
return nil, serviceerror.NewInternal(fmt.Sprintf("Unable to decode search attributes: %v", err))
943985
}
986+
// This is to prevent existing tasks to fail indefinitely.
987+
// If it's only invalid values error, then silently continue without them.
988+
searchAttributes, err = s.ValidateCustomSearchAttributes(searchAttributes)
989+
if err != nil {
990+
if _, ok := err.(*store.VisibilityStoreInvalidValuesError); !ok {
991+
return nil, err
992+
}
993+
}
944994
for saName, saValue := range searchAttributes {
945995
if saValue == nil {
946996
// If search attribute value is `nil`, it means that it shouldn't be added to the document.
@@ -1321,3 +1371,25 @@ func parsePageTokenValue(
13211371
))
13221372
}
13231373
}
1374+
1375+
func validateDatetime(value time.Time) error {
1376+
if value.Before(minTime) || value.After(maxTime) {
1377+
return serviceerror.NewInvalidArgument(
1378+
fmt.Sprintf("Date not supported in Elasticsearch: %v", value),
1379+
)
1380+
}
1381+
return nil
1382+
}
1383+
1384+
func validateString(value string) error {
1385+
if len(value) > maxStringLength {
1386+
return serviceerror.NewInvalidArgument(
1387+
fmt.Sprintf(
1388+
"Strings with more than %d bytes are not supported in Elasticsearch (got %s)",
1389+
maxStringLength,
1390+
value,
1391+
),
1392+
)
1393+
}
1394+
return nil
1395+
}

common/persistence/visibility/store/errors.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,33 @@
2525
package store
2626

2727
import (
28+
"strings"
29+
2830
"go.temporal.io/api/serviceerror"
2931
)
3032

33+
type (
34+
VisibilityStoreInvalidValuesError struct {
35+
errs []error
36+
}
37+
)
38+
3139
var (
3240
// OperationNotSupportedErr is returned when visibility operation in not supported.
3341
OperationNotSupportedErr = serviceerror.NewInvalidArgument("Operation not supported. Please use on Elasticsearch")
3442
)
43+
44+
func (e *VisibilityStoreInvalidValuesError) Error() string {
45+
var sb strings.Builder
46+
sb.WriteString("Visibility store invalid values errors: ")
47+
for _, err := range e.errs {
48+
sb.WriteString("[")
49+
sb.WriteString(err.Error())
50+
sb.WriteString("]")
51+
}
52+
return sb.String()
53+
}
54+
55+
func NewVisibilityStoreInvalidValuesError(errs []error) error {
56+
return &VisibilityStoreInvalidValuesError{errs: errs}
57+
}

common/persistence/visibility/store/sql/visibility_store.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ func (s *VisibilityStore) GetIndexName() string {
9292
return s.sqlStore.GetDbName()
9393
}
9494

95+
func (s *VisibilityStore) ValidateCustomSearchAttributes(
96+
searchAttributes map[string]any,
97+
) (map[string]any, error) {
98+
return searchAttributes, nil
99+
}
100+
95101
func (s *VisibilityStore) RecordWorkflowExecutionStarted(
96102
ctx context.Context,
97103
request *store.InternalRecordWorkflowExecutionStartedRequest,
@@ -514,6 +520,14 @@ func (s *VisibilityStore) prepareSearchAttributesForDb(
514520
if err != nil {
515521
return nil, err
516522
}
523+
// This is to prevent existing tasks to fail indefinitely.
524+
// If it's only invalid values error, then silently continue without them.
525+
searchAttributes, err = s.ValidateCustomSearchAttributes(searchAttributes)
526+
if err != nil {
527+
if _, ok := err.(*store.VisibilityStoreInvalidValuesError); !ok {
528+
return nil, err
529+
}
530+
}
517531

518532
for name, value := range searchAttributes {
519533
if value == nil {

common/persistence/visibility/store/standard/cassandra/visibility_store.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ func (v *visibilityStore) GetIndexName() string {
177177
return ""
178178
}
179179

180+
func (v *visibilityStore) ValidateCustomSearchAttributes(
181+
searchAttributes map[string]any,
182+
) (map[string]any, error) {
183+
return searchAttributes, nil
184+
}
185+
180186
// Close releases the resources held by this object
181187
func (v *visibilityStore) Close() {
182188
v.session.Close()

common/persistence/visibility/store/standard/sql/visibility_store.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ func (s *visibilityStore) GetIndexName() string {
8686
return ""
8787
}
8888

89+
func (s *visibilityStore) ValidateCustomSearchAttributes(
90+
searchAttributes map[string]any,
91+
) (map[string]any, error) {
92+
return searchAttributes, nil
93+
}
94+
8995
func (s *visibilityStore) RecordWorkflowExecutionStarted(
9096
ctx context.Context,
9197
request *store.InternalRecordWorkflowExecutionStartedRequest,

common/persistence/visibility/store/standard/visibility_store.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func (s *standardStore) GetIndexName() string {
7979
return ""
8080
}
8181

82+
func (s *standardStore) ValidateCustomSearchAttributes(
83+
searchAttributes map[string]any,
84+
) (map[string]any, error) {
85+
return searchAttributes, nil
86+
}
87+
8288
func (s *standardStore) RecordWorkflowExecutionStarted(
8389
ctx context.Context,
8490
request *store.InternalRecordWorkflowExecutionStartedRequest,

common/persistence/visibility/store/visibility_store.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ type (
4545
GetName() string
4646
GetIndexName() string
4747

48+
// Validate search attributes based on the store constraints. It returns a new map containing
49+
// only search attributes with valid values. If there are invalid values, an error of type
50+
// VisibilityStoreInvalidValuesError wraps all invalid values errors.
51+
ValidateCustomSearchAttributes(searchAttributes map[string]any) (map[string]any, error)
52+
4853
// Write APIs.
4954
RecordWorkflowExecutionStarted(ctx context.Context, request *InternalRecordWorkflowExecutionStartedRequest) error
5055
RecordWorkflowExecutionClosed(ctx context.Context, request *InternalRecordWorkflowExecutionClosedRequest) error

common/persistence/visibility/store/visibility_store_mock.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)