diff --git a/chasm/lib/workflow/library.go b/chasm/lib/workflow/library.go index eec510dac4a..8828339d724 100644 --- a/chasm/lib/workflow/library.go +++ b/chasm/lib/workflow/library.go @@ -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"), } } diff --git a/chasm/tree.go b/chasm/tree.go index 6b003728e59..559381db380 100644 --- a/chasm/tree.go +++ b/chasm/tree.go @@ -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 } @@ -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 } @@ -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. @@ -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)) { @@ -2743,7 +2746,7 @@ 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 } @@ -2751,8 +2754,14 @@ func (n *Node) ExecutePureTask( 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), diff --git a/chasm/tree_test.go b/chasm/tree_test.go index 7d1de490882..17b475819c5 100644 --- a/chasm/tree_test.go +++ b/chasm/tree_test.go @@ -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) { @@ -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) @@ -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{ @@ -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{ @@ -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) + } } }) } @@ -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() { diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index 9b15cffca0f..c84ba087aad 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -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()