mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed?
Remove nil search attributes from history events / mutable state / chasm
node
## Why?
Search attributes can be explicitly set as null on the following events:
* Workflow Start
* Upsert Search Attributes
* Continue-as-new
Starting a workflow or continuing a workflow as new with a nil search
attribute records the `{key: nil}` mapping in mutable state and the
history event.
Upserting search attributes erases any current search attribute value
from persistence (mutable state and visibility record).
Upserting the search attribute as nil in the SDK retains the mapping in
the WorkflowInfo as a {key: nil} mapping. On a continue-as-new event,
SDK sends this mapping to Server, and the search attribute gets
repopulated with the null search attribute in the history event and
mutable state.
This last scenario can cause inconsistency between the expected search
attributes and what is recorded in history. After upserting a nil search
attribute, the mutable state and visibility record no longer shows this
search attribute. But, once continuing as new, the search attribute
reappears, leading to unexpected NDE if the search attribute is also
deregistered.
## CHASM change
This also removes nil search attributes from CHASM nodes on replacement.
This mirrors the change to Workflow History Events and Mutable State.
## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [X] added new functional test(s)
281 lines
6.8 KiB
Go
281 lines
6.8 KiB
Go
package payload
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
)
|
|
|
|
type testStruct struct {
|
|
Int int
|
|
String string
|
|
Bytes []byte
|
|
}
|
|
|
|
func TestToString(t *testing.T) {
|
|
s := assert.New(t)
|
|
var result string
|
|
|
|
p := EncodeString("str")
|
|
result = ToString(p)
|
|
s.Equal(`"str"`, result)
|
|
|
|
p, err := Encode(10)
|
|
s.NoError(err)
|
|
result = ToString(p)
|
|
s.Equal("10", result)
|
|
|
|
p, err = Encode([]byte{41, 42, 43})
|
|
s.NoError(err)
|
|
result = ToString(p)
|
|
s.Equal("KSor", result)
|
|
|
|
p, err = Encode(&testStruct{
|
|
Int: 10,
|
|
String: "str",
|
|
Bytes: []byte{51, 52, 53},
|
|
})
|
|
s.NoError(err)
|
|
result = ToString(p)
|
|
s.Equal(`{"Int":10,"String":"str","Bytes":"MzQ1"}`, result)
|
|
|
|
p, err = Encode(nil)
|
|
s.NoError(err)
|
|
result = ToString(p)
|
|
s.Equal("nil", result)
|
|
|
|
result = ToString(nil)
|
|
s.Equal("", result)
|
|
}
|
|
|
|
func TestEncodeDecode(t *testing.T) {
|
|
s := assert.New(t)
|
|
|
|
var p *commonpb.Payload
|
|
var err error
|
|
|
|
var decodedValueStr string
|
|
valueStr := "asdf"
|
|
p, err = Encode(valueStr)
|
|
s.NoError(err)
|
|
err = Decode(p, &decodedValueStr)
|
|
s.NoError(err)
|
|
s.Equal(valueStr, decodedValueStr)
|
|
|
|
var decodedValueInt int64
|
|
valueInt := int64(1234)
|
|
p, err = Encode(valueInt)
|
|
s.NoError(err)
|
|
err = Decode(p, &decodedValueInt)
|
|
s.NoError(err)
|
|
s.Equal(valueInt, decodedValueInt)
|
|
|
|
var decodedValueBool bool
|
|
valueBool := true
|
|
p, err = Encode(valueBool)
|
|
s.NoError(err)
|
|
err = Decode(p, &decodedValueBool)
|
|
s.NoError(err)
|
|
s.Equal(valueBool, decodedValueBool)
|
|
|
|
var decodedValueAny any
|
|
var valueAny any = 123.45
|
|
p, err = Encode(valueAny)
|
|
s.NoError(err)
|
|
err = Decode(p, &decodedValueAny)
|
|
s.NoError(err)
|
|
s.Equal(valueAny, decodedValueAny)
|
|
}
|
|
|
|
func TestMergeMapOfPayload(t *testing.T) {
|
|
s := assert.New(t)
|
|
|
|
var currentMap map[string]*commonpb.Payload
|
|
var newMap map[string]*commonpb.Payload
|
|
resultMap := MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(newMap, resultMap)
|
|
|
|
newMap = make(map[string]*commonpb.Payload)
|
|
resultMap = MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(newMap, resultMap)
|
|
|
|
newMap = map[string]*commonpb.Payload{"key": EncodeString("val")}
|
|
resultMap = MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(newMap, resultMap)
|
|
|
|
newMap = map[string]*commonpb.Payload{
|
|
"key": EncodeString("val"),
|
|
"nil": nilPayload,
|
|
"emptyArray": emptySlicePayload,
|
|
}
|
|
resultMap = MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(map[string]*commonpb.Payload{"key": EncodeString("val")}, resultMap)
|
|
|
|
currentMap = map[string]*commonpb.Payload{"number": EncodeString("1")}
|
|
resultMap = MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(
|
|
map[string]*commonpb.Payload{"number": EncodeString("1"), "key": EncodeString("val")},
|
|
resultMap,
|
|
)
|
|
|
|
newValue, _ := Encode(nil)
|
|
newMap = map[string]*commonpb.Payload{"number": newValue}
|
|
resultMap = MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(0, len(resultMap))
|
|
|
|
newValue, _ = Encode([]int{})
|
|
newValue.Metadata["key"] = []byte("foo")
|
|
newMap = map[string]*commonpb.Payload{"number": newValue}
|
|
resultMap = MergeMapOfPayload(currentMap, newMap)
|
|
s.Equal(0, len(resultMap))
|
|
}
|
|
|
|
func TestIsEqual(t *testing.T) {
|
|
s := assert.New(t)
|
|
|
|
a, _ := Encode(nil)
|
|
b, _ := Encode(nil)
|
|
s.True(isEqual(a, b))
|
|
|
|
a, _ = Encode([]string{})
|
|
b, _ = Encode([]string{})
|
|
s.True(isEqual(a, b))
|
|
|
|
a.Metadata["key"] = []byte("foo")
|
|
b.Metadata["key"] = []byte("bar")
|
|
s.True(isEqual(a, b))
|
|
|
|
a, _ = Encode(nil)
|
|
b, _ = Encode([]string{})
|
|
s.False(isEqual(a, b))
|
|
|
|
a, _ = Encode([]string{})
|
|
b, _ = Encode("foo")
|
|
s.False(isEqual(a, b))
|
|
}
|
|
|
|
func TestFilterNilSearchAttributes(t *testing.T) {
|
|
s := assert.New(t)
|
|
|
|
// nil input returns nil
|
|
result := FilterNilSearchAttributes(nil)
|
|
s.Nil(result)
|
|
|
|
// empty SearchAttributes returns nil
|
|
emptySA := &commonpb.SearchAttributes{IndexedFields: map[string]*commonpb.Payload{}}
|
|
result = FilterNilSearchAttributes(emptySA)
|
|
s.Nil(result)
|
|
|
|
// SearchAttributes with only valid values returns filtered copy
|
|
validPayload := EncodeString("value")
|
|
saNonNil := &commonpb.SearchAttributes{
|
|
IndexedFields: map[string]*commonpb.Payload{
|
|
"key1": validPayload,
|
|
},
|
|
}
|
|
result = FilterNilSearchAttributes(saNonNil)
|
|
s.NotNil(result)
|
|
s.Len(result.IndexedFields, 1)
|
|
s.Equal(validPayload, result.IndexedFields["key1"])
|
|
|
|
// SearchAttributes with nil values filters them out
|
|
nilPayloadVal, _ := Encode(nil)
|
|
saMixed := &commonpb.SearchAttributes{
|
|
IndexedFields: map[string]*commonpb.Payload{
|
|
"valid": validPayload,
|
|
"nilVal": nilPayloadVal,
|
|
},
|
|
}
|
|
result = FilterNilSearchAttributes(saMixed)
|
|
s.NotNil(result)
|
|
s.Len(result.IndexedFields, 1)
|
|
s.Equal(validPayload, result.IndexedFields["valid"])
|
|
s.Nil(result.IndexedFields["nilVal"])
|
|
|
|
// SearchAttributes with empty slice values filters them out
|
|
emptySlicePayloadVal, _ := Encode([]string{})
|
|
saEmptySlice := &commonpb.SearchAttributes{
|
|
IndexedFields: map[string]*commonpb.Payload{
|
|
"valid": validPayload,
|
|
"emptySlice": emptySlicePayloadVal,
|
|
},
|
|
}
|
|
result = FilterNilSearchAttributes(saEmptySlice)
|
|
s.NotNil(result)
|
|
s.Len(result.IndexedFields, 1)
|
|
s.Equal(validPayload, result.IndexedFields["valid"])
|
|
|
|
// SearchAttributes with all nil/empty values returns nil
|
|
saAllNil := &commonpb.SearchAttributes{
|
|
IndexedFields: map[string]*commonpb.Payload{
|
|
"nil1": nilPayloadVal,
|
|
"nil2": emptySlicePayloadVal,
|
|
},
|
|
}
|
|
result = FilterNilSearchAttributes(saAllNil)
|
|
s.Nil(result)
|
|
}
|
|
|
|
func TestFilterNilMemo(t *testing.T) {
|
|
s := assert.New(t)
|
|
|
|
// nil input returns nil
|
|
result := FilterNilMemo(nil)
|
|
s.Nil(result)
|
|
|
|
// empty Memo returns nil
|
|
emptyMemo := &commonpb.Memo{Fields: map[string]*commonpb.Payload{}}
|
|
result = FilterNilMemo(emptyMemo)
|
|
s.Nil(result)
|
|
|
|
// Memo with only valid values returns filtered copy
|
|
validPayload := EncodeString("value")
|
|
memoNonNil := &commonpb.Memo{
|
|
Fields: map[string]*commonpb.Payload{
|
|
"key1": validPayload,
|
|
},
|
|
}
|
|
result = FilterNilMemo(memoNonNil)
|
|
s.NotNil(result)
|
|
s.Len(result.Fields, 1)
|
|
s.Equal(validPayload, result.Fields["key1"])
|
|
|
|
// Memo with nil values filters them out
|
|
nilPayloadVal, _ := Encode(nil)
|
|
memoMixed := &commonpb.Memo{
|
|
Fields: map[string]*commonpb.Payload{
|
|
"valid": validPayload,
|
|
"nilVal": nilPayloadVal,
|
|
},
|
|
}
|
|
result = FilterNilMemo(memoMixed)
|
|
s.NotNil(result)
|
|
s.Len(result.Fields, 1)
|
|
s.Equal(validPayload, result.Fields["valid"])
|
|
s.Nil(result.Fields["nilVal"])
|
|
|
|
// Memo with empty slice values filters them out
|
|
emptySlicePayloadVal, _ := Encode([]string{})
|
|
memoEmptySlice := &commonpb.Memo{
|
|
Fields: map[string]*commonpb.Payload{
|
|
"valid": validPayload,
|
|
"emptySlice": emptySlicePayloadVal,
|
|
},
|
|
}
|
|
result = FilterNilMemo(memoEmptySlice)
|
|
s.NotNil(result)
|
|
s.Len(result.Fields, 1)
|
|
s.Equal(validPayload, result.Fields["valid"])
|
|
|
|
// Memo with all nil/empty values returns nil
|
|
memoAllNil := &commonpb.Memo{
|
|
Fields: map[string]*commonpb.Payload{
|
|
"nil1": nilPayloadVal,
|
|
"nil2": emptySlicePayloadVal,
|
|
},
|
|
}
|
|
result = FilterNilMemo(memoAllNil)
|
|
s.Nil(result)
|
|
}
|