Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chasm/lib/workflow/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ func (l *Library) Name() string {

func (l *Library) Components() []*chasm.RegistrableComponent {
return []*chasm.RegistrableComponent{
chasm.NewRegistrableComponent[*Workflow]("Workflow"),
chasm.NewRegistrableComponent[*Workflow]("workflow"),
}
}
31 changes: 20 additions & 11 deletions chasm/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@ func (n *Node) Component(
return nil, errComponentNotFound
}

if err := node.prepareComponentValue(chasmContext); err != nil {
validationContext := NewContext(chasmContext.getContext(), node)
if err := node.prepareComponentValue(validationContext); err != nil {
return nil, err
}

Expand All @@ -379,18 +380,22 @@ func (n *Node) Component(
// Access check always begins on the target node's parent, and ignored for nodes
// without ancestors.
if node.parent != nil {
err := node.parent.validateAccess(chasmContext)
err := node.parent.validateAccess(validationContext)
if err != nil {
return nil, err
}
}

if ref.validationFn != nil {
if err := ref.validationFn(node.root().backend, chasmContext, componentValue); err != nil {
if err := ref.validationFn(node.root().backend, validationContext, componentValue); err != nil {
return nil, err
}
}

// prepare component value again using incoming context to mark node as dirty if needed.
if err := node.prepareComponentValue(chasmContext); err != nil {
return nil, err
}
return componentValue, nil
}

Expand Down Expand Up @@ -1169,7 +1174,7 @@ func unmarshalProto(

value := reflect.New(valueT.Elem())

if dataBlob == nil {
if dataBlob == nil || len(dataBlob.Data) == 0 {
// If the original data is the zero value of its type, the dataBlob loaded from persistence layer will be nil.
// But we know for component & data nodes, they won't get persisted in the first place if there's no data,
// so it must be a zero value.
Expand Down Expand Up @@ -2726,14 +2731,12 @@ func (n *Node) ExecutePureTask(
return false, fmt.Errorf("ExecutePureTask called on a SideEffect task '%s'", registrableTask.fqType())
}

ctx := NewMutableContext(
newContextWithOperationIntent(baseCtx, OperationIntentProgress),
n,
)
progressIntentCtx := newContextWithOperationIntent(baseCtx, OperationIntentProgress)
validationContext := NewContext(progressIntentCtx, n)

// Ensure this node's component value is hydrated before execution. Component
// will also check access rules.
component, err := n.Component(ctx, ComponentRef{})
_, err := n.Component(validationContext, ComponentRef{})
if err != nil {
// NotFound errors are expected here and we can safely skip the task execution.
if errors.As(err, new(*serviceerror.NotFound)) {
Expand All @@ -2743,16 +2746,22 @@ func (n *Node) ExecutePureTask(
}

// Run the task's registered value before execution.
valid, err := n.validateTask(ctx, taskAttributes, taskInstance)
valid, err := n.validateTask(validationContext, taskAttributes, taskInstance)
if err != nil {
return false, err
}
if !valid {
return false, nil
}

executionContext := NewMutableContext(progressIntentCtx, n)
component, err := n.Component(executionContext, ComponentRef{})
if err != nil {
return false, err
}

result := registrableTask.executeFn.Call([]reflect.Value{
reflect.ValueOf(ctx),
reflect.ValueOf(executionContext),
reflect.ValueOf(component),
reflect.ValueOf(taskAttributes),
reflect.ValueOf(taskInstance),
Expand Down
82 changes: 55 additions & 27 deletions chasm/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1475,10 +1475,8 @@ func (s *nodeSuite) TestValidateAccess() {
}

func (s *nodeSuite) TestGetComponent() {
root, err := s.newTestTree(testComponentSerializedNodes())
s.NoError(err)

errValidation := errors.New("some random validation error")

expectedTestComponent := &TestComponent{}
setTestComponentFields(expectedTestComponent, s.nodeBackend)
assertTestComponent := func(component Component) {
Expand All @@ -1492,39 +1490,47 @@ func (s *nodeSuite) TestGetComponent() {

testCases := []struct {
name string
chasmContext Context
chasmContextFn func(root *Node) Context
ref ComponentRef
expectedErr error
valueState valueState
nodeDirty bool
assertComponent func(Component)
}{
{
name: "path not found",
chasmContext: NewContext(context.Background(), root),
name: "path not found",
chasmContextFn: func(root *Node) Context {
return NewContext(context.Background(), root)
},
ref: ComponentRef{
componentPath: []string{"unknownComponent"},
},
expectedErr: errComponentNotFound,
},
{
name: "archetype mismatch",
chasmContext: NewContext(context.Background(), root),
name: "archetype mismatch",
chasmContextFn: func(root *Node) Context {
return NewContext(context.Background(), root)
},
ref: ComponentRef{
archetype: "TestLibrary.test_sub_component_1",
},
expectedErr: errComponentNotFound,
},
{
name: "entityGoType mismatch",
chasmContext: NewContext(context.Background(), root),
name: "entityGoType mismatch",
chasmContextFn: func(root *Node) Context {
return NewContext(context.Background(), root)
},
ref: ComponentRef{
entityGoType: reflect.TypeFor[*TestSubComponent2](),
},
expectedErr: errComponentNotFound,
},
{
name: "initialVT mismatch",
chasmContext: NewContext(context.Background(), root),
name: "initialVT mismatch",
chasmContextFn: func(root *Node) Context {
return NewMutableContext(context.Background(), root)
},
ref: ComponentRef{
componentPath: []string{"SubComponent1", "SubComponent11"},
// should be (1, 1) but we set it to (2, 2)
Expand All @@ -1536,8 +1542,10 @@ func (s *nodeSuite) TestGetComponent() {
expectedErr: errComponentNotFound,
},
{
name: "validation failure",
chasmContext: NewContext(context.Background(), root),
name: "validation failure",
chasmContextFn: func(root *Node) Context {
return NewMutableContext(context.Background(), root)
},
ref: ComponentRef{
componentPath: []string{"SubComponent1"},
componentInitialVT: &persistencespb.VersionedTransition{
Expand All @@ -1551,8 +1559,10 @@ func (s *nodeSuite) TestGetComponent() {
expectedErr: errValidation,
},
{
name: "success readonly access",
chasmContext: NewContext(context.Background(), root),
name: "success readonly access",
chasmContextFn: func(root *Node) Context {
return NewContext(context.Background(), root)
},
ref: ComponentRef{
componentPath: []string{}, // root
componentInitialVT: &persistencespb.VersionedTransition{
Expand All @@ -1564,32 +1574,42 @@ func (s *nodeSuite) TestGetComponent() {
},
},
expectedErr: nil,
valueState: valueStateSynced,
assertComponent: assertTestComponent,
},
{
name: "success mutable access",
chasmContext: NewMutableContext(context.Background(), root),
name: "success mutable access",
chasmContextFn: func(root *Node) Context {
return NewMutableContext(context.Background(), root)
},
ref: ComponentRef{
componentPath: []string{}, // root
},
expectedErr: nil,
valueState: valueStateNeedSyncStructure,
nodeDirty: true,
assertComponent: assertTestComponent,
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
component, err := root.Component(tc.chasmContext, tc.ref)
root, err := s.newTestTree(testComponentSerializedNodes())
s.NoError(err)

component, err := root.Component(tc.chasmContextFn(root), tc.ref)
s.Equal(tc.expectedErr, err)
if tc.expectedErr == nil {
// s.Equal(tc.expectedComponent, component)

node, ok := root.findNode(tc.ref.componentPath)
node, ok := root.findNode(tc.ref.componentPath)
if tc.expectedErr == nil {
s.True(ok)
s.Equal(component, node.value)
s.Equal(tc.valueState, node.valueState)
tc.assertComponent(component)
}

if ok {
if tc.nodeDirty {
s.Greater(node.valueState, valueStateSynced)
} else {
s.LessOrEqual(node.valueState, valueStateSynced)
}
}
})
}
Expand Down Expand Up @@ -2737,30 +2757,38 @@ func (s *nodeSuite) TestExecutePureTask() {
}

// Succeed task execution and validation (happy case).
root.setValueState(valueStateSynced)
expectExecute(nil)
expectValidate(true, nil)
executed, err := root.ExecutePureTask(ctx, taskAttributes, pureTask)
s.NoError(err)
s.True(executed)
s.Equal(valueStateNeedSyncStructure, root.valueState)

expectedErr := errors.New("dummy")

// Succeed validation, fail execution.
root.setValueState(valueStateSynced)
expectExecute(expectedErr)
expectValidate(true, nil)
_, err = root.ExecutePureTask(ctx, taskAttributes, pureTask)
s.ErrorIs(expectedErr, err)
s.Equal(valueStateNeedSyncStructure, root.valueState)

// Fail task validation (no execution occurs).
root.setValueState(valueStateSynced)
expectValidate(false, nil)
executed, err = root.ExecutePureTask(ctx, taskAttributes, pureTask)
s.NoError(err)
s.False(executed)
s.Equal(valueStateSynced, root.valueState) // task not executed, so node is clean

// Error during task validation (no execution occurs).
root.setValueState(valueStateSynced)
expectValidate(false, expectedErr)
_, err = root.ExecutePureTask(ctx, taskAttributes, pureTask)
s.ErrorIs(expectedErr, err)
s.Equal(valueStateSynced, root.valueState) // task not executed, so node is clean
}

func (s *nodeSuite) TestExecuteSideEffectTask() {
Expand Down
9 changes: 7 additions & 2 deletions service/history/workflow/mutable_state_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2586,8 +2586,13 @@ func (ms *MutableStateImpl) ApplyWorkflowExecutionStartedEvent(
// Initialize chasm tree once for new workflows.
// Using context.Background() because this is done outside an actual request context and the
// chasmworkflow.NewWorkflow does not actually use it currently.
mutableContext := chasm.NewMutableContext(context.Background(), ms.chasmTree.(*chasm.Node))
ms.chasmTree.(*chasm.Node).SetRootComponent(chasmworkflow.NewWorkflow(mutableContext, ms))
root, ok := ms.chasmTree.(*chasm.Node)
softassert.That(ms.logger, ok, "chasmTree cast failed")

if root.Archetype() == "" {
mutableContext := chasm.NewMutableContext(context.Background(), root)
root.SetRootComponent(chasmworkflow.NewWorkflow(mutableContext, ms))
}
}

event := startEvent.GetWorkflowExecutionStartedEventAttributes()
Expand Down
Loading