mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? Cleanup of `common/serialization` package: - unify several DataBlob encode/decode functions into 3 functions (see new codec.go) - unify function signatures to use `DataBlob` everywhere (instead of split []byte + EncodingType) ## Why? - less code - simpler - consistent - preparation step to allow changing proto to json encoding via env variable for debugging purposes ## How did you test it? - [ ] built - [ ] run locally and tested manually - [x] covered by existing tests - [ ] added new unit test(s) - [ ] added new functional test(s) ## Potential Risks See the two callouts for behavior difference.
2576 lines
78 KiB
Go
2576 lines
78 KiB
Go
//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination tree_mock.go
|
|
|
|
package chasm
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"fmt"
|
|
"iter"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
"go.temporal.io/api/serviceerror"
|
|
enumsspb "go.temporal.io/server/api/enums/v1"
|
|
persistencespb "go.temporal.io/server/api/persistence/v1"
|
|
"go.temporal.io/server/common/clock"
|
|
"go.temporal.io/server/common/definition"
|
|
"go.temporal.io/server/common/log"
|
|
"go.temporal.io/server/common/persistence"
|
|
"go.temporal.io/server/common/persistence/serialization"
|
|
"go.temporal.io/server/common/persistence/transitionhistory"
|
|
"go.temporal.io/server/common/softassert"
|
|
"go.temporal.io/server/service/history/tasks"
|
|
"golang.org/x/exp/maps"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
var (
|
|
protoMessageT = reflect.TypeFor[proto.Message]()
|
|
)
|
|
|
|
var (
|
|
errAccessCheckFailed = serviceerror.NewNotFound("access check failed, CHASM tree is closed for writes")
|
|
errComponentNotFound = serviceerror.NewNotFound("component not found")
|
|
errTaskNotValid = serviceerror.NewNotFound("task is no longer valid")
|
|
)
|
|
|
|
type valueState uint8
|
|
|
|
const (
|
|
valueStateUndefined valueState = iota
|
|
valueStateSynced
|
|
valueStateNeedDeserialize
|
|
valueStateNeedSerialize
|
|
)
|
|
|
|
const (
|
|
physicalTaskStatusNone int32 = iota
|
|
physicalTaskStatusCreated
|
|
)
|
|
|
|
type (
|
|
// Node is the in-memory representation of a persisted CHASM node.
|
|
//
|
|
// Node and all its methods are NOT meant to be used by CHASM component authors.
|
|
// They are exported for use by the CHASM engine and underlying MutableState implementation only.
|
|
Node struct {
|
|
*nodeBase
|
|
|
|
parent *Node
|
|
children map[string]*Node // child name (path segment) -> child node
|
|
nodeName string // key of this node in parent's children map, empty string for root node.
|
|
|
|
// Type of attributes controls the type of the node.
|
|
serializedNode *persistencespb.ChasmNode // serialized component | data | collection with metadata
|
|
value any // deserialized component | data | map
|
|
|
|
// valueState indicates if the value field and the persistence field serializedNode are in sync.
|
|
// If new value might be changed since it was deserialized and serialize method wasn't called yet, then valueState is valueStateNeedSerialize.
|
|
// If a node is constructed from the database, then valueState is valueStateNeedDeserialize.
|
|
// If serialize or deserialize method were called, then valueState is valueStateSynced, and next calls to them would be no-op.
|
|
// NOTE: This is a different concept from the IsDirty() method needed by MutableState which means
|
|
// if the state in memory matches the state in DB.
|
|
valueState valueState
|
|
|
|
// TODO: Consider storing encoded path for the node.
|
|
// Consider using unique package as well.
|
|
// Encoded path for different runs of the same Component type are the same.
|
|
//
|
|
// encodedPath string
|
|
|
|
// When terminated is true, regardless of the Lifecycle state of the component,
|
|
// the component will be considered as closed.
|
|
//
|
|
// This right now only applies to the root node and used to update MutableState
|
|
// executionState and executionStatus and trigger retention timers.
|
|
// We could consider extending the force terminate concept to sub-components as well.
|
|
terminated bool
|
|
}
|
|
|
|
// nodeBase is a set of dependencies and states shared by all nodes in a CHASM tree.
|
|
nodeBase struct {
|
|
registry *Registry
|
|
timeSource clock.TimeSource
|
|
backend NodeBackend
|
|
pathEncoder NodePathEncoder
|
|
logger log.Logger
|
|
|
|
// Following fields are changes accumulated in this transaction,
|
|
// and will get cleaned up after CloseTransaction().
|
|
|
|
// mutation field captures all user state changes (those will be replicated)
|
|
mutation NodesMutation
|
|
// systemMutation field captures all cell specific system changes (those will NOT be replicated)
|
|
systemMutation NodesMutation
|
|
newTasks map[any][]taskWithAttributes // component value -> task & attributes
|
|
}
|
|
|
|
taskWithAttributes struct {
|
|
task any
|
|
attributes TaskAttributes
|
|
}
|
|
|
|
// NodesMutation is a set of mutations for all nodes rooted at a given node n,
|
|
// including the node n itself.
|
|
//
|
|
// TODO: Return tree size changes in NodesMutation as well. MutateState needs to
|
|
// track the overall size of itself and terminate workflow if it exceeds the limit.
|
|
NodesMutation struct {
|
|
UpdatedNodes map[string]*persistencespb.ChasmNode // encoded node path -> chasm node
|
|
DeletedNodes map[string]struct{}
|
|
}
|
|
|
|
// NodesSnapshot is a snapshot for all nodes rooted at a given node n,
|
|
// including the node n itself.
|
|
NodesSnapshot struct {
|
|
Nodes map[string]*persistencespb.ChasmNode // encoded node path -> chasm node
|
|
}
|
|
|
|
// NodeBackend is a set of methods needed from MutableState
|
|
//
|
|
// This is for breaking cycle dependency between
|
|
// this package and service/history/workflow package
|
|
// where MutableState is defined.
|
|
NodeBackend interface {
|
|
// TODO: Add methods needed from MutateState here.
|
|
GetExecutionState() *persistencespb.WorkflowExecutionState
|
|
GetExecutionInfo() *persistencespb.WorkflowExecutionInfo
|
|
GetCurrentVersion() int64
|
|
NextTransitionCount() int64
|
|
CurrentVersionedTransition() *persistencespb.VersionedTransition
|
|
GetWorkflowKey() definition.WorkflowKey
|
|
AddTasks(...tasks.Task)
|
|
UpdateWorkflowStateStatus(
|
|
state enumsspb.WorkflowExecutionState,
|
|
status enumspb.WorkflowExecutionStatus,
|
|
) (bool, error)
|
|
}
|
|
|
|
// NodePathEncoder is an interface for encoding and decoding node paths.
|
|
// Logic outside the chasm package should only work with encoded paths.
|
|
NodePathEncoder interface {
|
|
Encode(node *Node, path []string) (string, error)
|
|
// TODO: Return a iterator on node name instead of []string,
|
|
// so that we can get a node by encoded path without additional
|
|
// allocation for the decoded path.
|
|
Decode(encodedPath string) ([]string, error)
|
|
}
|
|
|
|
// NodePureTask is intended to be implemented and used within the CHASM
|
|
// framework only.
|
|
NodePureTask interface {
|
|
ExecutePureTask(baseCtx context.Context, taskAttributes TaskAttributes, taskInstance any) error
|
|
ValidatePureTask(baseCtx context.Context, taskAttributes TaskAttributes, taskInstance any) (bool, error)
|
|
}
|
|
)
|
|
|
|
// NewTree creates a new in-memory CHASM tree from a collection of flattened persistence CHASM nodes.
|
|
func NewTree(
|
|
serializedNodes map[string]*persistencespb.ChasmNode, // This is coming from MS map[nodePath]ChasmNode.
|
|
|
|
registry *Registry,
|
|
timeSource clock.TimeSource,
|
|
backend NodeBackend,
|
|
pathEncoder NodePathEncoder,
|
|
logger log.Logger,
|
|
) (*Node, error) {
|
|
if len(serializedNodes) == 0 {
|
|
return NewEmptyTree(registry, timeSource, backend, pathEncoder, logger), nil
|
|
}
|
|
|
|
root := newTreeHelper(registry, timeSource, backend, pathEncoder, logger)
|
|
for encodedPath, serializedNode := range serializedNodes {
|
|
nodePath, err := pathEncoder.Decode(encodedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root.setSerializedNode(nodePath, serializedNode)
|
|
}
|
|
|
|
return root, nil
|
|
}
|
|
|
|
// NewEmptyTree creates a new empty in-memory CHASM tree.
|
|
func NewEmptyTree(
|
|
registry *Registry,
|
|
timeSource clock.TimeSource,
|
|
backend NodeBackend,
|
|
pathEncoder NodePathEncoder,
|
|
logger log.Logger,
|
|
) *Node {
|
|
root := newTreeHelper(registry, timeSource, backend, pathEncoder, logger)
|
|
|
|
// If serializedNodes is empty, it means that this new tree.
|
|
// Initialize empty serializedNode.
|
|
root.initSerializedNode(fieldTypeComponent)
|
|
// Although both value and serializedNode.Data are nil, they are considered NOT synced
|
|
// because value has no type and serializedNode does.
|
|
// deserialize method should set value when called.
|
|
root.valueState = valueStateNeedDeserialize
|
|
return root
|
|
}
|
|
|
|
func newTreeHelper(
|
|
registry *Registry,
|
|
timeSource clock.TimeSource,
|
|
backend NodeBackend,
|
|
pathEncoder NodePathEncoder,
|
|
logger log.Logger,
|
|
) *Node {
|
|
base := &nodeBase{
|
|
registry: registry,
|
|
timeSource: timeSource,
|
|
backend: backend,
|
|
pathEncoder: pathEncoder,
|
|
logger: logger,
|
|
|
|
mutation: NodesMutation{
|
|
UpdatedNodes: make(map[string]*persistencespb.ChasmNode),
|
|
DeletedNodes: make(map[string]struct{}),
|
|
},
|
|
systemMutation: NodesMutation{
|
|
UpdatedNodes: make(map[string]*persistencespb.ChasmNode),
|
|
DeletedNodes: make(map[string]struct{}),
|
|
},
|
|
newTasks: make(map[any][]taskWithAttributes),
|
|
}
|
|
|
|
return newNode(base, nil, "")
|
|
}
|
|
|
|
func (n *Node) SetRootComponent(
|
|
rootComponent Component,
|
|
) {
|
|
root := n.root()
|
|
root.value = rootComponent
|
|
root.valueState = valueStateNeedSerialize
|
|
}
|
|
|
|
// Component retrieves a component from the tree rooted at node n
|
|
// using the provided component reference
|
|
// It also performs access rule, and task validation checks
|
|
// (for task processing requests) before returning the component.
|
|
func (n *Node) Component(
|
|
chasmContext Context,
|
|
ref ComponentRef,
|
|
) (Component, error) {
|
|
if ref.entityGoType != nil && ref.archetype == "" {
|
|
rootRC, ok := n.registry.componentOf(ref.entityGoType)
|
|
if !ok {
|
|
return nil, errComponentNotFound
|
|
}
|
|
ref.archetype = Archetype(rootRC.fqType())
|
|
|
|
}
|
|
if ref.archetype != "" &&
|
|
n.root().serializedNode.GetMetadata().GetComponentAttributes().Type != ref.archetype.String() {
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
node, ok := n.findNode(ref.componentPath)
|
|
if !ok {
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
metadata := node.serializedNode.Metadata
|
|
if ref.componentInitialVT != nil && transitionhistory.Compare(
|
|
ref.componentInitialVT,
|
|
metadata.InitialVersionedTransition,
|
|
) != 0 {
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
if err := node.prepareComponentValue(chasmContext); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
componentValue, ok := node.value.(Component)
|
|
if !ok {
|
|
return nil, serviceerror.NewInternalf(
|
|
"component value is not of type Component: %v", reflect.TypeOf(node.value),
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if ref.validationFn != nil {
|
|
if err := ref.validationFn(node.root().backend, chasmContext, componentValue); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return componentValue, nil
|
|
}
|
|
|
|
// validateAccess performs the access rule check on a node.
|
|
//
|
|
// When the context's intent is OperationIntentProgress, This check validates that
|
|
// all of a node's ancestors are still in a running state, and can accept writes. In
|
|
// the case of a newly-created node, a detached node, or an OperationIntentObserve
|
|
// intent, the check is skipped.
|
|
func (n *Node) validateAccess(ctx Context) error {
|
|
intent := operationIntentFromContext(ctx.getContext())
|
|
if intent != OperationIntentProgress {
|
|
// Read-only operations are always allowed.
|
|
return nil
|
|
}
|
|
|
|
// TODO - check if this is a detached node, operations are always allowed.
|
|
|
|
if n.parent != nil {
|
|
err := n.parent.validateAccess(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Only Component nodes need to be validated.
|
|
if n.serializedNode.Metadata.GetComponentAttributes() == nil {
|
|
return nil
|
|
}
|
|
|
|
// Hydrate the component so we can access its LifecycleState.
|
|
err := n.prepareComponentValue(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
componentValue, _ := n.value.(Component) //nolint:revive // unchecked-type-assertion
|
|
|
|
if componentValue.LifecycleState(ctx).IsClosed() {
|
|
return errAccessCheckFailed
|
|
}
|
|
|
|
if n.terminated {
|
|
// Terminated nodes can never be written to.
|
|
return errAccessCheckFailed
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) prepareComponentValue(
|
|
chasmContext Context,
|
|
) error {
|
|
metadata := n.serializedNode.Metadata
|
|
componentAttr := metadata.GetComponentAttributes()
|
|
if componentAttr == nil {
|
|
return serviceerror.NewInternalf(
|
|
"expect chasm node to have ComponentAttributes, actual attributes: %v", metadata.Attributes,
|
|
)
|
|
}
|
|
|
|
if n.valueState == valueStateNeedDeserialize {
|
|
registrableComponent, ok := n.registry.component(componentAttr.GetType())
|
|
if !ok {
|
|
return serviceerror.NewInternalf("component type name not registered: %v", componentAttr.GetType())
|
|
}
|
|
|
|
if err := n.deserialize(registrableComponent.goType); err != nil {
|
|
return fmt.Errorf("failed to deserialize component: %w", err)
|
|
}
|
|
}
|
|
|
|
// For now, we assume if a node is accessed with a MutableContext,
|
|
// its value will be mutated and no longer in sync with the serializedNode.
|
|
_, componentCanBeMutated := chasmContext.(MutableContext)
|
|
if componentCanBeMutated {
|
|
n.valueState = valueStateNeedSerialize
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) prepareDataValue(
|
|
chasmContext Context,
|
|
valueT reflect.Type,
|
|
) error {
|
|
metadata := n.serializedNode.Metadata
|
|
dataAttr := metadata.GetDataAttributes()
|
|
if dataAttr == nil {
|
|
return serviceerror.NewInternalf(
|
|
"expect chasm node to have DataAttributes, actual attributes: %v", metadata.Attributes,
|
|
)
|
|
}
|
|
|
|
if n.valueState == valueStateNeedDeserialize {
|
|
if err := n.deserialize(valueT); err != nil {
|
|
return fmt.Errorf("failed to deserialize data: %w", err)
|
|
}
|
|
}
|
|
|
|
// For now, we assume if a node is accessed with a MutableContext,
|
|
// its value will be mutated and no longer in sync with the serializedNode.
|
|
_, componentCanBeMutated := chasmContext.(MutableContext)
|
|
if componentCanBeMutated {
|
|
n.valueState = valueStateNeedSerialize
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) preparePointerValue(
|
|
chasmContext Context,
|
|
) error {
|
|
metadata := n.serializedNode.Metadata
|
|
pointerAttr := metadata.GetPointerAttributes()
|
|
if pointerAttr == nil {
|
|
return serviceerror.NewInternal(
|
|
fmt.Sprintf("expect chasm node to have PointerAttributes, actual attributes: %v", metadata.Attributes),
|
|
)
|
|
}
|
|
|
|
if n.valueState == valueStateNeedDeserialize {
|
|
if err := n.deserialize(nil); err != nil {
|
|
return fmt.Errorf("failed to deserialize data: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) fieldType() fieldType {
|
|
if n.serializedNode.GetMetadata().GetComponentAttributes() != nil {
|
|
return fieldTypeComponent
|
|
}
|
|
|
|
if n.serializedNode.GetMetadata().GetDataAttributes() != nil {
|
|
return fieldTypeData
|
|
}
|
|
|
|
if n.serializedNode.GetMetadata().GetPointerAttributes() != nil {
|
|
return fieldTypePointer
|
|
}
|
|
|
|
if n.serializedNode.GetMetadata().GetCollectionAttributes() != nil {
|
|
softassert.Fail(n.logger, "fieldType can't be called on Collection node because Collection is not a Field")
|
|
}
|
|
|
|
return fieldTypeUnspecified
|
|
}
|
|
|
|
func (n *Node) valueFields() iter.Seq[fieldInfo] {
|
|
return fieldsOf(reflect.ValueOf(n.value))
|
|
}
|
|
|
|
func assertStructPointer(t reflect.Type) error {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
|
|
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
|
|
return serviceerror.NewInternalf("only pointer to struct is supported for tree node value: got %s", t.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) initSerializedNode(ft fieldType) {
|
|
switch ft {
|
|
case fieldTypeData:
|
|
n.serializedNode = &persistencespb.ChasmNode{
|
|
Metadata: &persistencespb.ChasmNodeMetadata{
|
|
InitialVersionedTransition: &persistencespb.VersionedTransition{
|
|
TransitionCount: n.backend.NextTransitionCount(),
|
|
NamespaceFailoverVersion: n.backend.GetCurrentVersion(),
|
|
},
|
|
Attributes: &persistencespb.ChasmNodeMetadata_DataAttributes{
|
|
DataAttributes: &persistencespb.ChasmDataAttributes{},
|
|
},
|
|
},
|
|
}
|
|
case fieldTypeComponent:
|
|
n.serializedNode = &persistencespb.ChasmNode{
|
|
Metadata: &persistencespb.ChasmNodeMetadata{
|
|
InitialVersionedTransition: &persistencespb.VersionedTransition{
|
|
TransitionCount: n.backend.NextTransitionCount(),
|
|
NamespaceFailoverVersion: n.backend.GetCurrentVersion(),
|
|
},
|
|
Attributes: &persistencespb.ChasmNodeMetadata_ComponentAttributes{
|
|
ComponentAttributes: &persistencespb.ChasmComponentAttributes{},
|
|
},
|
|
},
|
|
}
|
|
case fieldTypePointer, fieldTypeDeferredPointer:
|
|
// A deferred pointer will be resolved to a regular pointer before persistence.
|
|
n.serializedNode = &persistencespb.ChasmNode{
|
|
Metadata: &persistencespb.ChasmNodeMetadata{
|
|
InitialVersionedTransition: &persistencespb.VersionedTransition{
|
|
TransitionCount: n.backend.NextTransitionCount(),
|
|
NamespaceFailoverVersion: n.backend.GetCurrentVersion(),
|
|
},
|
|
Attributes: &persistencespb.ChasmNodeMetadata_PointerAttributes{
|
|
PointerAttributes: &persistencespb.ChasmPointerAttributes{},
|
|
},
|
|
},
|
|
}
|
|
case fieldTypeUnspecified:
|
|
softassert.Fail(n.logger, fmt.Sprintf("initSerializedNode can't be called with %v", ft))
|
|
}
|
|
}
|
|
|
|
func (n *Node) initSerializedCollectionNode() {
|
|
n.serializedNode = &persistencespb.ChasmNode{
|
|
Metadata: &persistencespb.ChasmNodeMetadata{
|
|
InitialVersionedTransition: &persistencespb.VersionedTransition{
|
|
TransitionCount: n.backend.NextTransitionCount(),
|
|
NamespaceFailoverVersion: n.backend.GetCurrentVersion(),
|
|
},
|
|
Attributes: &persistencespb.ChasmNodeMetadata_CollectionAttributes{
|
|
CollectionAttributes: &persistencespb.ChasmCollectionAttributes{},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (n *Node) setSerializedNode(
|
|
nodePath []string,
|
|
serializedNode *persistencespb.ChasmNode,
|
|
) *Node {
|
|
if len(nodePath) == 0 {
|
|
n.serializedNode = serializedNode
|
|
n.valueState = valueStateNeedDeserialize
|
|
return n
|
|
}
|
|
|
|
childName := nodePath[0]
|
|
childNode, ok := n.children[childName]
|
|
if !ok {
|
|
childNode = newNode(n.nodeBase, n, childName)
|
|
n.children[childName] = childNode
|
|
}
|
|
return childNode.setSerializedNode(nodePath[1:], serializedNode)
|
|
}
|
|
|
|
// serialize sets or updates serializedValue field of the node n with serialized value.
|
|
func (n *Node) serialize() error {
|
|
if n.valueState != valueStateNeedSerialize {
|
|
return nil
|
|
}
|
|
|
|
switch n.serializedNode.GetMetadata().GetAttributes().(type) {
|
|
case *persistencespb.ChasmNodeMetadata_ComponentAttributes:
|
|
return n.serializeComponentNode()
|
|
case *persistencespb.ChasmNodeMetadata_DataAttributes:
|
|
return n.serializeDataNode()
|
|
case *persistencespb.ChasmNodeMetadata_CollectionAttributes:
|
|
return n.serializeCollectionNode()
|
|
case *persistencespb.ChasmNodeMetadata_PointerAttributes:
|
|
return n.serializePointerNode()
|
|
default:
|
|
return serviceerror.NewInternal("unknown node type")
|
|
}
|
|
}
|
|
|
|
func (n *Node) serializeComponentNode() error {
|
|
for field := range n.valueFields() {
|
|
if field.err != nil {
|
|
return field.err
|
|
}
|
|
|
|
if field.kind != fieldKindData {
|
|
continue
|
|
}
|
|
|
|
var blob *commonpb.DataBlob
|
|
if !field.val.IsNil() {
|
|
var err error
|
|
if blob, err = serialization.ProtoEncode(field.val.Interface().(proto.Message)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
rc, ok := n.registry.componentFor(n.value)
|
|
if !ok {
|
|
return serviceerror.NewInternalf("component type %s is not registered", reflect.TypeOf(n.value).String())
|
|
}
|
|
|
|
n.serializedNode.Data = blob
|
|
n.serializedNode.GetMetadata().GetComponentAttributes().Type = rc.fqType()
|
|
n.updateLastUpdateVersionedTransition()
|
|
n.valueState = valueStateSynced
|
|
|
|
// continue to iterate over fields to validate that there is only one proto field in the component.
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// syncSubComponents syncs the entire tree recursively (starting from the root node n) from the underlining component value:
|
|
// - Create:
|
|
// -- if child node is nil but subcomponent is not empty or key present in the collection,
|
|
// a new node with subcomponent/collection_item value is created.
|
|
// - Delete:
|
|
// -- if subcomponent is empty, the corresponding child is removed from the tree,
|
|
// -- if subcomponent is no longer in a component, the corresponding child is removed from the tree,
|
|
// -- if collection item is not in the collection, the corresponding child is removed from the tree,
|
|
// -- when a child is removed, all its children are removed too.
|
|
//
|
|
// All removed paths are added to mutation.DeletedNodes (which is shared between all nodes in the tree).
|
|
//
|
|
// True is returned when CHASM must perform deferred pointer resolution.
|
|
func (n *Node) syncSubComponents() (bool, error) {
|
|
if n.parent != nil {
|
|
return false, serviceerror.NewInternal("syncSubComponents must be called on root node")
|
|
}
|
|
// If node value is nil, then it means there are no subcomponents to sync.
|
|
if n.value == nil {
|
|
return false, nil
|
|
}
|
|
return n.syncSubComponentsInternal(rootPath)
|
|
}
|
|
|
|
// syncSubComponentsInternal syncs a subcomponent's fields, managing the
|
|
// associated node lifecycles. True is returned when CHASM must perform deferred
|
|
// pointer resolution.
|
|
func (n *Node) syncSubComponentsInternal(
|
|
nodePath []string,
|
|
) (needsPointerResolution bool, err error) {
|
|
childrenToKeep := make(map[string]struct{})
|
|
for field := range n.valueFields() {
|
|
if field.err != nil {
|
|
return false, field.err
|
|
}
|
|
|
|
switch field.kind {
|
|
case fieldKindUnspecified:
|
|
softassert.Fail(n.logger, "field.kind can be unspecified only if err is not nil, and there is a check for it above")
|
|
case fieldKindData:
|
|
// Nothing to sync.
|
|
case fieldKindSubField:
|
|
keepChild, updatedFieldV, needsResolve, err := n.syncSubField(field.val, field.name, nodePath)
|
|
needsPointerResolution = needsPointerResolution || needsResolve
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if updatedFieldV.IsValid() {
|
|
field.val.Set(updatedFieldV)
|
|
}
|
|
if keepChild {
|
|
childrenToKeep[field.name] = struct{}{}
|
|
}
|
|
case fieldKindSubMap:
|
|
if field.val.IsNil() {
|
|
// If Map field is nil then delete all collection items nodes and collection node itself.
|
|
continue
|
|
}
|
|
|
|
collectionNode := n.children[field.name]
|
|
if collectionNode == nil {
|
|
collectionNode = newNode(n.nodeBase, n, field.name)
|
|
collectionNode.initSerializedCollectionNode()
|
|
collectionNode.valueState = valueStateNeedSerialize
|
|
n.children[field.name] = collectionNode
|
|
}
|
|
|
|
// Validate map type.
|
|
if field.val.Kind() != reflect.Map {
|
|
errMsg := fmt.Sprintf("CHASM map must be of map type: value of %s is not of a map type", n.nodeName)
|
|
softassert.Fail(n.logger, errMsg)
|
|
return false, serviceerror.NewInternal(errMsg)
|
|
}
|
|
|
|
if len(field.val.MapKeys()) == 0 {
|
|
// If Map field is empty then delete all collection items nodes and collection node itself.
|
|
continue
|
|
}
|
|
|
|
mapValT := field.typ.Elem()
|
|
if mapValT.Kind() != reflect.Struct || genericTypePrefix(mapValT) != chasmFieldTypePrefix {
|
|
errMsg := fmt.Sprintf("CHASM map value must be of Field[T] type: %s collection value type is not Field[T] but %s", n.nodeName, mapValT)
|
|
softassert.Fail(n.logger, errMsg)
|
|
return false, serviceerror.NewInternal(errMsg)
|
|
}
|
|
|
|
collectionItemsToKeep := make(map[string]struct{})
|
|
for _, mapKeyV := range field.val.MapKeys() {
|
|
mapItemV := field.val.MapIndex(mapKeyV)
|
|
collectionKey, err := n.mapKeyToString(mapKeyV)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
keepItem, updatedMapItemV, needsResolve, err := collectionNode.syncSubField(mapItemV, collectionKey, append(nodePath, field.name))
|
|
needsPointerResolution = needsPointerResolution || needsResolve
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if updatedMapItemV.IsValid() {
|
|
// The only way to update item in the map is to set it back.
|
|
field.val.SetMapIndex(mapKeyV, updatedMapItemV)
|
|
}
|
|
if keepItem {
|
|
collectionItemsToKeep[collectionKey] = struct{}{}
|
|
}
|
|
}
|
|
if err := collectionNode.deleteChildren(collectionItemsToKeep, append(nodePath, field.name)); err != nil {
|
|
return false, err
|
|
}
|
|
childrenToKeep[field.name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
err = n.deleteChildren(childrenToKeep, nodePath)
|
|
return needsPointerResolution, err
|
|
}
|
|
|
|
func (n *Node) mapKeyToString(keyV reflect.Value) (string, error) {
|
|
switch keyV.Kind() {
|
|
case reflect.String:
|
|
return keyV.String(), nil
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return strconv.FormatInt(keyV.Int(), 10), nil
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return strconv.FormatUint(keyV.Uint(), 10), nil
|
|
case reflect.Bool:
|
|
return strconv.FormatBool(keyV.Bool()), nil
|
|
default:
|
|
errMsg := fmt.Sprintf("CHASM map key type for node %s must be one of [%s], got %s", n.nodeName, mapKeyTypes, keyV.Type().String())
|
|
softassert.Fail(n.logger, errMsg)
|
|
return "", serviceerror.NewInternal(errMsg)
|
|
}
|
|
}
|
|
|
|
func (n *Node) stringToMapKey(nodeName string, key string, keyT reflect.Type) (reflect.Value, error) {
|
|
var (
|
|
keyV reflect.Value
|
|
err error
|
|
)
|
|
switch keyT.Kind() {
|
|
case reflect.String:
|
|
keyV = reflect.ValueOf(key)
|
|
case reflect.Int:
|
|
var x int64
|
|
x, err = strconv.ParseInt(key, 10, 0)
|
|
keyV = reflect.ValueOf(int(x))
|
|
case reflect.Int8:
|
|
var x int64
|
|
x, err = strconv.ParseInt(key, 10, 8)
|
|
keyV = reflect.ValueOf(int8(x))
|
|
case reflect.Int16:
|
|
var x int64
|
|
x, err = strconv.ParseInt(key, 10, 16)
|
|
keyV = reflect.ValueOf(int16(x))
|
|
case reflect.Int32:
|
|
var x int64
|
|
x, err = strconv.ParseInt(key, 10, 32)
|
|
keyV = reflect.ValueOf(int32(x))
|
|
case reflect.Int64:
|
|
var x int64
|
|
x, err = strconv.ParseInt(key, 10, 64)
|
|
keyV = reflect.ValueOf(x)
|
|
case reflect.Uint:
|
|
var x uint64
|
|
x, err = strconv.ParseUint(key, 10, 0)
|
|
keyV = reflect.ValueOf(uint(x))
|
|
case reflect.Uint8:
|
|
var x uint64
|
|
x, err = strconv.ParseUint(key, 10, 8)
|
|
keyV = reflect.ValueOf(uint8(x))
|
|
case reflect.Uint16:
|
|
var x uint64
|
|
x, err = strconv.ParseUint(key, 10, 16)
|
|
keyV = reflect.ValueOf(uint16(x))
|
|
case reflect.Uint32:
|
|
var x uint64
|
|
x, err = strconv.ParseUint(key, 10, 32)
|
|
keyV = reflect.ValueOf(uint32(x))
|
|
case reflect.Uint64:
|
|
var x uint64
|
|
x, err = strconv.ParseUint(key, 10, 64)
|
|
keyV = reflect.ValueOf(x)
|
|
case reflect.Bool:
|
|
var b bool
|
|
b, err = strconv.ParseBool(key)
|
|
keyV = reflect.ValueOf(b)
|
|
default:
|
|
err = fmt.Errorf("unsupported type %s of kind %s: supported key types: %s", keyT.String(), keyT.Kind().String(), mapKeyTypes)
|
|
softassert.Fail(n.logger, err.Error())
|
|
// Use softassert only here because this is the only case that indicates "compile" time error.
|
|
// The other errors below can come from data type mismatch between a component and persisted data.
|
|
}
|
|
|
|
if err == nil && !keyV.IsValid() {
|
|
err = fmt.Errorf("value %s is not valid of type %s of kind %s", key, keyT.String(), keyT.Kind().String())
|
|
}
|
|
|
|
if err != nil {
|
|
err = serviceerror.NewInternalf("serialized map %s key value %s can't be parsed to CHASM map key type %s: %s", nodeName, key, keyT.String(), err.Error())
|
|
}
|
|
|
|
return keyV, err
|
|
}
|
|
|
|
// syncSubField syncs node n with value from fieldV parameter.
|
|
// If fieldV is a component, then it will sync all subcomponents recursively.
|
|
// It returns:
|
|
// - bool keepNode indicates if node needs to be removed from parent's children map.
|
|
// - updatedFieldV if fieldV needs to be updated with new value.
|
|
// If updatedFieldV is invalid, then fieldV doesn't need to be updated.
|
|
// NOTE: this function doesn't update fieldV because it might come from the map which is not addressable.
|
|
// - needsPointerResolution indicates if a new deferred pointer has been added,
|
|
// in which case CHASM needs to resolve it as part of the current transaction.
|
|
// - error.
|
|
func (n *Node) syncSubField(
|
|
fieldV reflect.Value,
|
|
fieldN string,
|
|
nodePath []string,
|
|
) (
|
|
keepNode bool,
|
|
updatedFieldV reflect.Value,
|
|
needsPointerResolution bool,
|
|
err error,
|
|
) {
|
|
internalV := fieldV.FieldByName(internalFieldName)
|
|
//nolint:revive // Internal field is guaranteed to be of type fieldInternal.
|
|
internal := internalV.Interface().(fieldInternal)
|
|
if internal.isEmpty() {
|
|
// Internal is empty only when Field was explicitly set to NewEmptyField[T] which is a way to clear its value.
|
|
// In this case, return keepNode=false and this node (and all it children) will be added to DeletedNodes map.
|
|
return
|
|
}
|
|
if internal.node == nil && internal.value() != nil {
|
|
// Field is not empty but tree node is not set. It means this is a new field, and a node must be created.
|
|
childNode := newNode(n.nodeBase, n, fieldN)
|
|
|
|
switch internal.fieldType() {
|
|
case fieldTypePointer:
|
|
if _, ok := internal.value().([]string); !ok {
|
|
err = serviceerror.NewInternalf("value must be of type []string for the field of pointer type: got %T", internal.value())
|
|
return
|
|
}
|
|
case fieldTypeData, fieldTypeComponent:
|
|
if err = assertStructPointer(reflect.TypeOf(internal.value())); err != nil {
|
|
return
|
|
}
|
|
case fieldTypeDeferredPointer:
|
|
// No-op, validation happens when the pointer is resolved.
|
|
needsPointerResolution = true
|
|
default:
|
|
err = serviceerror.NewInternalf("unexpected field type: %d", internal.fieldType())
|
|
return
|
|
}
|
|
childNode.value = internal.value()
|
|
childNode.initSerializedNode(internal.fieldType())
|
|
childNode.valueState = valueStateNeedSerialize
|
|
|
|
n.children[fieldN] = childNode
|
|
internal.node = childNode
|
|
|
|
updatedFieldV = reflect.New(fieldV.Type()).Elem()
|
|
updatedFieldV.FieldByName(internalFieldName).Set(reflect.ValueOf(internal))
|
|
}
|
|
if internal.fieldType() == fieldTypeComponent && internal.value() != nil {
|
|
needsPointerResolution, err = internal.node.syncSubComponentsInternal(append(nodePath, fieldN))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return true, updatedFieldV, needsPointerResolution, nil
|
|
}
|
|
|
|
func (n *Node) deleteChildren(childrenToKeep map[string]struct{}, currentPath []string) error {
|
|
for childName, childNode := range n.children {
|
|
if _, childToKeep := childrenToKeep[childName]; !childToKeep {
|
|
if err := childNode.deleteChildren(nil, append(currentPath, childName)); err != nil {
|
|
return err
|
|
}
|
|
path, err := n.pathEncoder.Encode(childNode, append(currentPath, childName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.mutation.DeletedNodes[path] = struct{}{}
|
|
// If a parent is about to be removed, it must not have any children.
|
|
softassert.That(n.logger, len(childNode.children) == 0, "childNode.children must be empty when childNode is removed")
|
|
delete(n.children, childName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) serializeDataNode() error {
|
|
protoValue, ok := n.value.(proto.Message)
|
|
if !ok {
|
|
return serviceerror.NewInternal("only support proto.Message as chasm data")
|
|
}
|
|
|
|
var blob *commonpb.DataBlob
|
|
if protoValue != nil {
|
|
var err error
|
|
if blob, err = serialization.ProtoEncode(protoValue); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
n.serializedNode.Data = blob
|
|
n.updateLastUpdateVersionedTransition()
|
|
n.valueState = valueStateSynced
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) serializeCollectionNode() error {
|
|
// The collection node has no data; therefore, only metadata needs to be updated.
|
|
n.updateLastUpdateVersionedTransition()
|
|
n.valueState = valueStateSynced
|
|
return nil
|
|
}
|
|
|
|
// serializePointerNode doesn't serialize anything but named this way for consistency.
|
|
func (n *Node) serializePointerNode() error {
|
|
path, isPathValid := n.value.([]string)
|
|
if !isPathValid {
|
|
msg := fmt.Sprintf("pointer path is not []string but %T for node %s", n.value, n.nodeName)
|
|
softassert.Fail(n.logger, msg)
|
|
return serviceerror.NewInternal(msg)
|
|
}
|
|
|
|
n.serializedNode.GetMetadata().GetPointerAttributes().NodePath = path
|
|
n.updateLastUpdateVersionedTransition()
|
|
n.valueState = valueStateSynced
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) updateLastUpdateVersionedTransition() {
|
|
if n.serializedNode.GetMetadata().GetLastUpdateVersionedTransition() == nil {
|
|
n.serializedNode.GetMetadata().LastUpdateVersionedTransition = &persistencespb.VersionedTransition{}
|
|
}
|
|
n.serializedNode.GetMetadata().GetLastUpdateVersionedTransition().TransitionCount = n.backend.NextTransitionCount()
|
|
n.serializedNode.GetMetadata().GetLastUpdateVersionedTransition().NamespaceFailoverVersion = n.backend.GetCurrentVersion()
|
|
}
|
|
|
|
// deserialize initializes the node's value from its serializedNode.
|
|
// If a value is of the component type, it initializes every chasm.Field of it and sets serializedNode field but not value field,
|
|
// i.e., it doesn't deserialize recursively and must be called on every node separately.
|
|
// valueT must be a pointer to a concrete type (not interface). To support deserialization of a component to interface,
|
|
// a registry lookup must be done outside the deserialize method.
|
|
func (n *Node) deserialize(
|
|
valueT reflect.Type,
|
|
) error {
|
|
if err := assertStructPointer(valueT); err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.valueState != valueStateNeedDeserialize {
|
|
return nil
|
|
}
|
|
|
|
switch n.serializedNode.GetMetadata().GetAttributes().(type) {
|
|
case *persistencespb.ChasmNodeMetadata_ComponentAttributes:
|
|
return n.deserializeComponentNode(valueT)
|
|
case *persistencespb.ChasmNodeMetadata_DataAttributes:
|
|
return n.deserializeDataNode(valueT)
|
|
case *persistencespb.ChasmNodeMetadata_CollectionAttributes:
|
|
softassert.Fail(n.logger, "deserialize shouldn't be called on the collection node because it is deserialized with the parent component.")
|
|
case *persistencespb.ChasmNodeMetadata_PointerAttributes:
|
|
return n.deserializePointerNode()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) deserializeComponentNode(
|
|
valueT reflect.Type,
|
|
) error {
|
|
// valueT is guaranteed to be a pointer to the struct because it was already validated by the assertStructPointer method.
|
|
valueV := reflect.New(valueT.Elem())
|
|
|
|
for field := range fieldsOf(valueV) {
|
|
if field.err != nil {
|
|
return field.err
|
|
}
|
|
|
|
switch field.kind {
|
|
case fieldKindUnspecified:
|
|
softassert.Fail(n.logger, "field.kind can be unspecified only if err is not nil, and there is a check for it above")
|
|
case fieldKindData:
|
|
value, err := unmarshalProto(n.serializedNode.GetData(), field.typ)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
field.val.Set(value)
|
|
case fieldKindSubField:
|
|
if childNode, found := n.children[field.name]; found {
|
|
chasmFieldV := reflect.New(field.typ).Elem()
|
|
internalValue := reflect.ValueOf(newFieldInternalWithNode(childNode))
|
|
chasmFieldV.FieldByName(internalFieldName).Set(internalValue)
|
|
field.val.Set(chasmFieldV)
|
|
}
|
|
case fieldKindSubMap:
|
|
if collectionNode, found := n.children[field.name]; found {
|
|
mapFieldV := field.val
|
|
if mapFieldV.IsNil() {
|
|
mapFieldV = reflect.MakeMapWithSize(field.typ, field.val.Len())
|
|
field.val.Set(mapFieldV)
|
|
}
|
|
|
|
for collectionItemName, collectionItemNode := range collectionNode.children {
|
|
// field.typ.Elem() is a go type of map item: Field[T]
|
|
chasmFieldV := reflect.New(field.typ.Elem()).Elem()
|
|
internalValue := reflect.ValueOf(newFieldInternalWithNode(collectionItemNode))
|
|
chasmFieldV.FieldByName(internalFieldName).Set(internalValue)
|
|
mapKeyV, err := n.stringToMapKey(field.name, collectionItemName, mapFieldV.Type().Key())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mapFieldV.SetMapIndex(mapKeyV, chasmFieldV)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
n.value = valueV.Interface()
|
|
n.valueState = valueStateSynced
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) deserializeDataNode(
|
|
valueT reflect.Type,
|
|
) error {
|
|
value, err := unmarshalProto(n.serializedNode.GetData(), valueT)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.value = value.Interface()
|
|
n.valueState = valueStateSynced
|
|
return nil
|
|
}
|
|
|
|
// deserializePointerNode doesn't deserialize anything but named this way for consistency.
|
|
func (n *Node) deserializePointerNode() error {
|
|
n.value = n.serializedNode.GetMetadata().GetPointerAttributes().GetNodePath()
|
|
n.valueState = valueStateSynced
|
|
return nil
|
|
}
|
|
|
|
func unmarshalProto(
|
|
dataBlob *commonpb.DataBlob,
|
|
valueT reflect.Type,
|
|
) (reflect.Value, error) {
|
|
if !valueT.AssignableTo(protoMessageT) {
|
|
return reflect.Value{}, serviceerror.NewInternal("only support proto.Message as chasm data")
|
|
}
|
|
|
|
value := reflect.New(valueT.Elem())
|
|
|
|
if dataBlob == nil {
|
|
// 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.
|
|
dataBlob = &commonpb.DataBlob{
|
|
EncodingType: enumspb.ENCODING_TYPE_PROTO3,
|
|
Data: []byte{},
|
|
}
|
|
}
|
|
|
|
if err := serialization.Decode(dataBlob, value.Interface().(proto.Message)); err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// Ref implements the CHASM Context interface
|
|
func (n *Node) Ref(
|
|
component Component,
|
|
) ([]byte, error) {
|
|
// No need to update tree structure here. If a Component can only be found after
|
|
// syncSubComponents() is called, it means the component is created in the
|
|
// current transition and don't have a reference yet.
|
|
|
|
for path, node := range n.andAllChildren() {
|
|
if node.value == component {
|
|
workflowKey := node.backend.GetWorkflowKey()
|
|
ref := ComponentRef{
|
|
EntityKey: EntityKey{
|
|
NamespaceID: workflowKey.NamespaceID,
|
|
BusinessID: workflowKey.WorkflowID,
|
|
EntityID: workflowKey.RunID,
|
|
},
|
|
archetype: n.Archetype(),
|
|
// TODO: Consider using node's LastUpdateVersionedTransition for checking staleness here.
|
|
// Using VersionedTransition of the entire tree might be too strict.
|
|
entityLastUpdateVT: transitionhistory.CopyVersionedTransition(node.backend.CurrentVersionedTransition()),
|
|
componentPath: path,
|
|
componentInitialVT: node.serializedNode.GetMetadata().GetInitialVersionedTransition(),
|
|
}
|
|
return ref.Serialize(n.registry)
|
|
}
|
|
}
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
// componentNodePath implements the CHASM Context interface
|
|
func (n *Node) componentNodePath(
|
|
component Component,
|
|
) ([]string, error) {
|
|
// It's unnecessary to deserialize entire tree as calling this method means
|
|
// caller already have the deserialized value.
|
|
for path, node := range n.andAllChildren() {
|
|
if node.fieldType() != fieldTypeComponent {
|
|
continue
|
|
}
|
|
|
|
if node.value == component {
|
|
return path, nil
|
|
}
|
|
}
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
// dataNodePath implements the CHASM Context interface
|
|
func (n *Node) dataNodePath(
|
|
data proto.Message,
|
|
) ([]string, error) {
|
|
// It's unnecessary to deserialize entire tree as calling this method means
|
|
// caller already have the deserialized value.
|
|
for path, node := range n.andAllChildren() {
|
|
if node.fieldType() != fieldTypeData {
|
|
continue
|
|
}
|
|
|
|
if node.value == data {
|
|
return path, nil
|
|
}
|
|
}
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
// Now implements the CHASM Context interface
|
|
func (n *Node) Now(
|
|
_ Component,
|
|
) time.Time {
|
|
// TODO: Now() could be different for components after we support Pause for CHASM components.
|
|
return n.timeSource.Now()
|
|
}
|
|
|
|
// AddTask implements the CHASM MutableContext interface
|
|
func (n *Node) AddTask(
|
|
component Component,
|
|
taskAttributes TaskAttributes,
|
|
task any,
|
|
) {
|
|
n.nodeBase.newTasks[component] = append(n.nodeBase.newTasks[component], taskWithAttributes{
|
|
task: task,
|
|
attributes: taskAttributes,
|
|
})
|
|
}
|
|
|
|
// CloseTransaction is used by MutableState to close the transaction and
|
|
// track changes made in the current transaction.
|
|
func (n *Node) CloseTransaction() (NodesMutation, error) {
|
|
defer n.cleanupTransaction()
|
|
|
|
// When closing the transaction, we no longer need to differentiate between system mutations and user mutations.
|
|
// Both of them need to be returned and persisted.
|
|
maps.Copy(n.mutation.UpdatedNodes, n.systemMutation.UpdatedNodes)
|
|
maps.Copy(n.mutation.DeletedNodes, n.systemMutation.DeletedNodes)
|
|
|
|
needsPointerResolution, err := n.syncSubComponents()
|
|
if err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
|
|
if needsPointerResolution {
|
|
if err := n.resolveDeferredPointers(); err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
}
|
|
|
|
nextVersionedTransition := &persistencespb.VersionedTransition{
|
|
NamespaceFailoverVersion: n.backend.GetCurrentVersion(),
|
|
TransitionCount: n.backend.NextTransitionCount(),
|
|
}
|
|
|
|
rootLifecycleChanged, err := n.closeTransactionHandleRootLifecycleChange()
|
|
if err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
|
|
if rootLifecycleChanged {
|
|
if err := n.closeTransactionForceUpdateVisibility(nextVersionedTransition); err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
}
|
|
|
|
if err := n.closeTransactionSerializeNodes(); err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
|
|
if err := n.closeTransactionUpdateComponentTasks(nextVersionedTransition); err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
|
|
if err := n.closeTransactionGeneratePhysicalSideEffectTasks(); err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
|
|
if err := n.closeTransactionGeneratePhysicalPureTask(); err != nil {
|
|
return NodesMutation{}, err
|
|
}
|
|
|
|
return n.mutation, nil
|
|
}
|
|
|
|
func (n *Node) closeTransactionHandleRootLifecycleChange() (bool, error) {
|
|
if n.valueState != valueStateNeedSerialize {
|
|
return false, nil
|
|
}
|
|
|
|
if n.terminated {
|
|
return n.backend.UpdateWorkflowStateStatus(
|
|
enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED,
|
|
enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED,
|
|
)
|
|
}
|
|
|
|
chasmContext := NewContext(context.Background(), n)
|
|
component, err := n.Component(chasmContext, ComponentRef{})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
lifecycleState := component.LifecycleState(chasmContext)
|
|
|
|
var newState enumsspb.WorkflowExecutionState
|
|
var newStatus enumspb.WorkflowExecutionStatus
|
|
switch lifecycleState {
|
|
case LifecycleStateRunning:
|
|
newState = enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING
|
|
newStatus = enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING
|
|
case LifecycleStateCompleted:
|
|
newState = enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED
|
|
newStatus = enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED
|
|
case LifecycleStateFailed:
|
|
newState = enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED
|
|
newStatus = enumspb.WORKFLOW_EXECUTION_STATUS_FAILED
|
|
default:
|
|
return false, serviceerror.NewInternalf("unknown component lifecycle state: %v", lifecycleState)
|
|
}
|
|
|
|
return n.backend.UpdateWorkflowStateStatus(newState, newStatus)
|
|
}
|
|
|
|
func (n *Node) closeTransactionForceUpdateVisibility(
|
|
nextVersionedTransition *persistencespb.VersionedTransition,
|
|
) error {
|
|
for _, child := range n.children {
|
|
componentAttr := child.serializedNode.Metadata.GetComponentAttributes()
|
|
if componentAttr == nil ||
|
|
componentAttr.Type != visibilityComponentFqType ||
|
|
transitionhistory.Compare(child.serializedNode.Metadata.LastUpdateVersionedTransition, nextVersionedTransition) == 0 {
|
|
// Skip force update is the node is not a visibility component or already updated in this transition.
|
|
continue
|
|
}
|
|
|
|
mutableContext := NewMutableContext(context.Background(), n)
|
|
visComponent, err := child.Component(mutableContext, ComponentRef{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
visibility, ok := visComponent.(*Visibility)
|
|
if !ok {
|
|
return serviceerror.NewInternalf("expected visibility component for component with type %s, but got %T", visibilityComponentFqType, visComponent)
|
|
}
|
|
visibility.GenerateTask(mutableContext)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) closeTransactionSerializeNodes() error {
|
|
for nodePath, node := range n.andAllChildren() {
|
|
if node.valueState != valueStateNeedSerialize {
|
|
continue
|
|
}
|
|
if err := node.serialize(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if componentAttr := node.serializedNode.GetMetadata().GetComponentAttributes(); componentAttr != nil &&
|
|
componentAttr.Type == visibilityComponentFqType &&
|
|
len(nodePath) != 1 {
|
|
return serviceerror.NewInternalf("CHASM visibility component must be immeidate child of the root node, but found at path %s", nodePath)
|
|
}
|
|
|
|
encodedPath, err := n.pathEncoder.Encode(node, nodePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.mutation.UpdatedNodes[encodedPath] = node.serializedNode
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:revive // cognitive complexity 28 (> max enabled 25)
|
|
func (n *Node) closeTransactionUpdateComponentTasks(
|
|
nextVersionedTransition *persistencespb.VersionedTransition,
|
|
) error {
|
|
taskOffset := int64(1)
|
|
validateContext := NewContext(context.Background(), n)
|
|
|
|
for _, node := range n.andAllChildren() {
|
|
// no-op if node is not a component
|
|
componentAttr := node.serializedNode.Metadata.GetComponentAttributes()
|
|
if componentAttr == nil {
|
|
continue
|
|
}
|
|
|
|
// no-op if node is not updated in this transition
|
|
// This also prevents standby logic from updating component tasks, since the condition
|
|
// will never be true.
|
|
lastUpdateVT := node.serializedNode.GetMetadata().LastUpdateVersionedTransition
|
|
if transitionhistory.Compare(lastUpdateVT, nextVersionedTransition) != 0 {
|
|
continue
|
|
}
|
|
|
|
// no-op if node is not even deserialized
|
|
// NOTE: do not check if node.valueState == valueStateNeedSerialize here, because this method needs to be called
|
|
// after the tree structure is updated and value is serialized, and that flag will
|
|
// get set to valueStateSynced.
|
|
if node.valueState == valueStateNeedDeserialize {
|
|
continue
|
|
}
|
|
|
|
// Validate existing tasks and remove invalid ones.
|
|
var validationErr error
|
|
deleteFunc := func(existingTask *persistencespb.ChasmComponentAttributes_Task) bool {
|
|
existingTaskInstance, err := node.deserializeComponentTask(existingTask)
|
|
if err != nil {
|
|
validationErr = err
|
|
return false
|
|
}
|
|
|
|
valid, err := node.validateTask(
|
|
validateContext,
|
|
TaskAttributes{
|
|
ScheduledTime: existingTask.ScheduledTime.AsTime(),
|
|
Destination: existingTask.Destination,
|
|
},
|
|
existingTaskInstance,
|
|
)
|
|
if err != nil {
|
|
validationErr = err
|
|
return false
|
|
}
|
|
return !valid
|
|
}
|
|
componentAttr.SideEffectTasks = slices.DeleteFunc(componentAttr.SideEffectTasks, deleteFunc)
|
|
if validationErr != nil {
|
|
return validationErr
|
|
}
|
|
componentAttr.PureTasks = slices.DeleteFunc(componentAttr.PureTasks, deleteFunc)
|
|
if validationErr != nil {
|
|
return validationErr
|
|
}
|
|
|
|
// no-op if no new tasks for this component
|
|
newTasks, ok := node.nodeBase.newTasks[node.value]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, newTask := range newTasks {
|
|
valid, err := node.validateTask(
|
|
validateContext,
|
|
newTask.attributes,
|
|
newTask.task,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !valid {
|
|
continue
|
|
}
|
|
|
|
taskValue := newTask.task
|
|
registrableTask, ok := n.registry.taskFor(taskValue)
|
|
if !ok {
|
|
return serviceerror.NewInternalf("task type %s is not registered", reflect.TypeOf(taskValue).String())
|
|
}
|
|
|
|
taskBlob, err := serializeTask(registrableTask, taskValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
componentTask := &persistencespb.ChasmComponentAttributes_Task{
|
|
Type: registrableTask.fqType(),
|
|
Destination: newTask.attributes.Destination,
|
|
ScheduledTime: timestamppb.New(newTask.attributes.ScheduledTime),
|
|
Data: taskBlob,
|
|
VersionedTransition: nextVersionedTransition,
|
|
VersionedTransitionOffset: taskOffset,
|
|
PhysicalTaskStatus: physicalTaskStatusNone,
|
|
}
|
|
|
|
if registrableTask.isPureTask {
|
|
componentAttr.PureTasks = append(componentAttr.PureTasks, componentTask)
|
|
} else {
|
|
componentAttr.SideEffectTasks = append(componentAttr.SideEffectTasks, componentTask)
|
|
}
|
|
|
|
taskOffset++
|
|
}
|
|
|
|
// pure tasks are sorted by scheduled time.
|
|
slices.SortFunc(componentAttr.PureTasks, comparePureTasks)
|
|
}
|
|
|
|
// TODO: We cannot simply assert that all tasks in n.nodeBase.newTasks are processed.
|
|
// That should be the case when only one transition for each transaction.
|
|
// However, when processing pure tasks, we run multiple pure tasks, thus multiple transitions
|
|
// in one transaction. This means it's possible that task generated for a component in the first
|
|
// task, and that component get deleted by the second task.
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) deserializeComponentTask(
|
|
componentTask *persistencespb.ChasmComponentAttributes_Task,
|
|
) (any, error) {
|
|
registableTask, ok := n.registry.task(componentTask.Type)
|
|
if !ok {
|
|
return nil, serviceerror.NewInternalf("task type %s is not registered", componentTask.Type)
|
|
}
|
|
|
|
// TODO: cache deserialized task value (reflect.Value) in the node,
|
|
// use task VT and offset as the key
|
|
taskValue, err := deserializeTask(registableTask, componentTask.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return taskValue.Interface(), nil
|
|
}
|
|
|
|
// validateTask runs taskInstance's registered validation handler.
|
|
func (n *Node) validateTask(
|
|
validateContext Context,
|
|
taskAttributes TaskAttributes,
|
|
taskInstance any,
|
|
) (bool, error) {
|
|
registableTask, ok := n.registry.taskFor(taskInstance)
|
|
if !ok {
|
|
return false, serviceerror.NewInternalf(
|
|
"task type for goType %s is not registered", reflect.TypeOf(taskInstance).Name())
|
|
}
|
|
|
|
retValues := registableTask.validateFn.Call([]reflect.Value{
|
|
reflect.ValueOf(validateContext),
|
|
reflect.ValueOf(n.value),
|
|
reflect.ValueOf(taskAttributes),
|
|
reflect.ValueOf(taskInstance),
|
|
})
|
|
if !retValues[1].IsNil() {
|
|
//revive:disable-next-line:unchecked-type-assertion
|
|
return false, retValues[1].Interface().(error)
|
|
}
|
|
//revive:disable-next-line:unchecked-type-assertion
|
|
return retValues[0].Interface().(bool), nil
|
|
}
|
|
|
|
func (n *Node) closeTransactionGeneratePhysicalSideEffectTasks() error {
|
|
entityKey := n.backend.GetWorkflowKey()
|
|
|
|
for encodedPath, updatedNode := range n.mutation.UpdatedNodes {
|
|
componentAttr := updatedNode.GetMetadata().GetComponentAttributes()
|
|
if componentAttr == nil {
|
|
continue
|
|
}
|
|
|
|
sideEffectTasks := componentAttr.GetSideEffectTasks()
|
|
for idx := len(sideEffectTasks) - 1; idx >= 0; idx-- {
|
|
sideEffectTask := sideEffectTasks[idx]
|
|
if sideEffectTask.PhysicalTaskStatus == physicalTaskStatusCreated {
|
|
break
|
|
}
|
|
|
|
category, err := taskCategory(sideEffectTask)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
physicalTask := &tasks.ChasmTask{
|
|
WorkflowKey: entityKey,
|
|
VisibilityTimestamp: sideEffectTask.ScheduledTime.AsTime(),
|
|
Destination: sideEffectTask.Destination,
|
|
Category: category,
|
|
Info: &persistencespb.ChasmTaskInfo{
|
|
ComponentInitialVersionedTransition: updatedNode.Metadata.InitialVersionedTransition,
|
|
ComponentLastUpdateVersionedTransition: updatedNode.Metadata.LastUpdateVersionedTransition,
|
|
Path: encodedPath,
|
|
Type: sideEffectTask.Type,
|
|
Data: sideEffectTask.Data,
|
|
},
|
|
}
|
|
n.backend.AddTasks(physicalTask)
|
|
sideEffectTask.PhysicalTaskStatus = physicalTaskStatusCreated
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) closeTransactionGeneratePhysicalPureTask() error {
|
|
var firstPureTask *persistencespb.ChasmComponentAttributes_Task
|
|
var firstTaskNode *Node
|
|
for _, node := range n.andAllChildren() {
|
|
componentAttr := node.serializedNode.GetMetadata().GetComponentAttributes()
|
|
if componentAttr == nil {
|
|
continue
|
|
}
|
|
|
|
pureTasks := componentAttr.GetPureTasks()
|
|
if len(pureTasks) == 0 {
|
|
continue
|
|
}
|
|
|
|
if firstPureTask == nil ||
|
|
comparePureTasks(pureTasks[0], firstPureTask) < 0 {
|
|
firstPureTask = pureTasks[0]
|
|
firstTaskNode = node
|
|
}
|
|
}
|
|
|
|
if firstPureTask == nil || firstPureTask.PhysicalTaskStatus == physicalTaskStatusCreated {
|
|
return nil
|
|
}
|
|
|
|
n.backend.AddTasks(&tasks.ChasmTaskPure{
|
|
WorkflowKey: n.backend.GetWorkflowKey(),
|
|
VisibilityTimestamp: firstPureTask.ScheduledTime.AsTime(),
|
|
Category: tasks.CategoryTimer,
|
|
})
|
|
|
|
// We need to persist the task status change as well, so add the node
|
|
// to the list of updated nodes.
|
|
// However, since task status is a cluster local field, we don't really
|
|
// update LastUpdateVersionedTransition for this node, and the change won't be replicated.
|
|
firstPureTask.PhysicalTaskStatus = physicalTaskStatusCreated
|
|
encodedPath, err := firstTaskNode.encodedPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.mutation.UpdatedNodes[encodedPath] = firstTaskNode.serializedNode
|
|
return nil
|
|
}
|
|
|
|
// resolveDeferredPointers resolves all deferred pointers in the tree.
|
|
// Returns error if any deferred pointer cannot be resolved, as deferred pointers
|
|
// cannot be persisted after transaction close.
|
|
func (n *Node) resolveDeferredPointers() error {
|
|
for _, node := range n.andAllChildren() {
|
|
if node.value == nil || node.fieldType() != fieldTypeComponent {
|
|
continue
|
|
}
|
|
|
|
for field := range node.valueFields() {
|
|
if field.err != nil {
|
|
return field.err
|
|
}
|
|
|
|
if field.kind != fieldKindSubField {
|
|
continue
|
|
}
|
|
|
|
internalV := field.val.FieldByName(internalFieldName)
|
|
internal, _ := internalV.Interface().(fieldInternal) //nolint:revive
|
|
|
|
if internal.fieldType() == fieldTypeDeferredPointer && internal.value() != nil {
|
|
// Must resolve the deferred pointer or fail the transaction.
|
|
var resolvedPath []string
|
|
var err error
|
|
|
|
switch value := internal.value().(type) {
|
|
case Component:
|
|
resolvedPath, err = n.componentNodePath(value)
|
|
case proto.Message:
|
|
resolvedPath, err = n.dataNodePath(value)
|
|
default:
|
|
err = serviceerror.NewInternalf("unable to create a deferred pointer for values of type: %T", value)
|
|
}
|
|
if err != nil {
|
|
return serviceerror.NewInternalf("failed to resolve deferred pointer during transaction close: %v", err)
|
|
}
|
|
|
|
// Update the field to be a regular pointer, reusing the existing serializedNode,
|
|
// and update the serializedNode's value.
|
|
newInternal := newFieldInternalWithValue(fieldTypePointer, resolvedPath)
|
|
newInternal.node = internal.node
|
|
newInternal.node.value = resolvedPath
|
|
internalV.Set(reflect.ValueOf(newInternal))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// andAllChildren returns a sequence of all nodes in the tree starting from n, including n itself.
|
|
// The sequence is depth-first, pre-order traversal.
|
|
func (n *Node) andAllChildren() iter.Seq2[[]string, *Node] {
|
|
return func(yield func([]string, *Node) bool) {
|
|
var walk func([]string, *Node) bool
|
|
walk = func(path []string, node *Node) bool {
|
|
if node == nil {
|
|
return true
|
|
}
|
|
if !yield(path, node) {
|
|
return false
|
|
}
|
|
for _, child := range node.children {
|
|
if !walk(append(path, child.nodeName), child) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
walk(nil, n)
|
|
}
|
|
}
|
|
|
|
func (n *Node) cleanupTransaction() {
|
|
n.mutation = NodesMutation{
|
|
UpdatedNodes: make(map[string]*persistencespb.ChasmNode),
|
|
DeletedNodes: make(map[string]struct{}),
|
|
}
|
|
|
|
// System mutation are most likely to be empty, so we reuse existing ones if possible.
|
|
if len(n.systemMutation.UpdatedNodes) != 0 {
|
|
n.systemMutation.UpdatedNodes = make(map[string]*persistencespb.ChasmNode)
|
|
}
|
|
if len(n.systemMutation.DeletedNodes) != 0 {
|
|
n.systemMutation.DeletedNodes = make(map[string]struct{})
|
|
}
|
|
|
|
n.newTasks = make(map[any][]taskWithAttributes)
|
|
}
|
|
|
|
// Snapshot returns all nodes in the tree that have been modified after the given min versioned transition.
|
|
// A nil exclusiveMinVT will be treated as the same as the zero versioned transition and returns all nodes in the tree.
|
|
// This method should only be invoked on root CHASM node when IsDirty() is false.
|
|
func (n *Node) Snapshot(
|
|
exclusiveMinVT *persistencespb.VersionedTransition,
|
|
) NodesSnapshot {
|
|
if !softassert.That(n.logger, n.parent == nil, "chasm.Snapshot() should only be called on the root node") {
|
|
panic(fmt.Sprintf("chasm.Snapshot() called on child node: %+v", n))
|
|
}
|
|
|
|
// TODO: add assertion on IsDirty() once implemented
|
|
|
|
nodes := make(map[string]*persistencespb.ChasmNode)
|
|
n.snapshotInternal(exclusiveMinVT, []string{}, nodes)
|
|
|
|
return NodesSnapshot{
|
|
Nodes: nodes,
|
|
}
|
|
}
|
|
|
|
func (n *Node) snapshotInternal(
|
|
exclusiveMinVT *persistencespb.VersionedTransition,
|
|
currentPath []string,
|
|
nodes map[string]*persistencespb.ChasmNode,
|
|
) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
|
|
if transitionhistory.Compare(n.serializedNode.Metadata.LastUpdateVersionedTransition, exclusiveMinVT) > 0 {
|
|
encodedPath, err := n.pathEncoder.Encode(n, currentPath)
|
|
if !softassert.That(n.logger, err == nil, "chasm path encoding should always succeed on clean tree") {
|
|
panic(fmt.Sprintf("failed to encode chasm path on clean tree: %v", err))
|
|
}
|
|
nodes[encodedPath] = n.serializedNode
|
|
}
|
|
|
|
for childName, childNode := range n.children {
|
|
childNode.snapshotInternal(
|
|
exclusiveMinVT,
|
|
append(currentPath, childName),
|
|
nodes,
|
|
)
|
|
}
|
|
}
|
|
|
|
// ApplyMutation is used by replication stack to apply node
|
|
// mutations from the source cluster.
|
|
//
|
|
// NOTE: It will be an error if UpdatedNodes and DeletedNodes have overlapping keys,
|
|
// as the CHASM tree does not have enough information to tell if the deletion happens
|
|
// before or after the update.
|
|
func (n *Node) ApplyMutation(
|
|
mutation NodesMutation,
|
|
) error {
|
|
if err := n.applyDeletions(mutation.DeletedNodes); err != nil {
|
|
return err
|
|
}
|
|
|
|
return n.applyUpdates(mutation.UpdatedNodes)
|
|
}
|
|
|
|
// ApplySnapshot is used by replication stack to apply node
|
|
// snapshot from the source cluster.
|
|
//
|
|
// If we simply substituting the entire CHASM tree, we will be
|
|
// forced to close the transaction as snapshot and potentially
|
|
// write extra data to persistence.
|
|
// This method will instead figure out the mutations needed to
|
|
// bring the current tree to the be the same as the snapshot,
|
|
// thus allowing us to close the transaction as mutation.
|
|
func (n *Node) ApplySnapshot(
|
|
incomingSnapshot NodesSnapshot,
|
|
) error {
|
|
currentSnapshot := n.Snapshot(nil)
|
|
|
|
mutation := NodesMutation{
|
|
UpdatedNodes: make(map[string]*persistencespb.ChasmNode),
|
|
DeletedNodes: make(map[string]struct{}),
|
|
}
|
|
|
|
for encodedPath := range currentSnapshot.Nodes {
|
|
if _, ok := incomingSnapshot.Nodes[encodedPath]; !ok {
|
|
mutation.DeletedNodes[encodedPath] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for encodedPath, incomingNode := range incomingSnapshot.Nodes {
|
|
currentNode, ok := currentSnapshot.Nodes[encodedPath]
|
|
if !ok {
|
|
mutation.UpdatedNodes[encodedPath] = incomingNode
|
|
continue
|
|
}
|
|
|
|
if transitionhistory.Compare(
|
|
currentNode.Metadata.LastUpdateVersionedTransition,
|
|
incomingNode.Metadata.LastUpdateVersionedTransition,
|
|
) != 0 {
|
|
mutation.UpdatedNodes[encodedPath] = incomingNode
|
|
}
|
|
}
|
|
|
|
return n.ApplyMutation(mutation)
|
|
}
|
|
|
|
func (n *Node) applyDeletions(
|
|
deletedNodes map[string]struct{},
|
|
) error {
|
|
for encodedPath := range deletedNodes {
|
|
path, err := n.pathEncoder.Decode(encodedPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
node, ok := n.findNode(path)
|
|
if !ok {
|
|
// Already deleted.
|
|
// This could happen when:
|
|
// - If the mutations passed in include changes
|
|
// older than the current state of the tree.
|
|
// - We are already applied the deletion on a parent node.
|
|
continue
|
|
}
|
|
|
|
if err := node.delete(path, &encodedPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) applyUpdates(
|
|
updatedNodes map[string]*persistencespb.ChasmNode,
|
|
) error {
|
|
for encodedPath, updatedNode := range updatedNodes {
|
|
path, err := n.pathEncoder.Decode(encodedPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
node, ok := n.findNode(path)
|
|
if !ok {
|
|
// Node doesn't exist, we need to create it.
|
|
newNode := n.setSerializedNode(path, updatedNode)
|
|
newNode.resetTaskStatus()
|
|
n.mutation.UpdatedNodes[encodedPath] = newNode.serializedNode
|
|
continue
|
|
}
|
|
|
|
// An empty node may be created when child update is applied before the parent,
|
|
// in which case node.serializedNode will be nil.
|
|
if node.serializedNode == nil || transitionhistory.Compare(
|
|
node.serializedNode.Metadata.LastUpdateVersionedTransition,
|
|
updatedNode.Metadata.LastUpdateVersionedTransition,
|
|
) != 0 {
|
|
localComponentAttr := node.serializedNode.GetMetadata().GetComponentAttributes()
|
|
updatedComponentAttr := updatedNode.GetMetadata().GetComponentAttributes()
|
|
if localComponentAttr != nil && updatedComponentAttr != nil {
|
|
carryOverTaskStatus(
|
|
localComponentAttr.SideEffectTasks,
|
|
updatedComponentAttr.SideEffectTasks,
|
|
compareSideEffectTasks,
|
|
)
|
|
carryOverTaskStatus(
|
|
localComponentAttr.PureTasks,
|
|
updatedComponentAttr.PureTasks,
|
|
comparePureTasks,
|
|
)
|
|
}
|
|
|
|
n.mutation.UpdatedNodes[encodedPath] = updatedNode
|
|
node.serializedNode = updatedNode
|
|
node.value = nil
|
|
node.valueState = valueStateNeedDeserialize
|
|
|
|
// Clearing decoded value for ancestor nodes is not necessary because the value field is not referenced directly.
|
|
// Parent node is pointing to the Node struct.
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) RefreshTasks() error {
|
|
for _, node := range n.andAllChildren() {
|
|
// Only reset task status here, the actual task generation will be done when
|
|
// CloseTransaction() is called to persist the changes.
|
|
if reset := node.resetTaskStatus(); !reset {
|
|
continue
|
|
}
|
|
|
|
encodedPath, err := node.encodedPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Task status is a cluster local field and changes to it doesn't need to be replicated.
|
|
// Do not here update LastUpdateVersionedTransition for the node.
|
|
// Record the changes in system mutation so that it can be persisted.
|
|
n.systemMutation.UpdatedNodes[encodedPath] = node.serializedNode
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) resetTaskStatus() bool {
|
|
if n.serializedNode == nil || n.serializedNode.GetMetadata() == nil {
|
|
return false
|
|
}
|
|
|
|
componentAttr := n.serializedNode.GetMetadata().GetComponentAttributes()
|
|
if componentAttr == nil {
|
|
return false
|
|
}
|
|
|
|
reset := false
|
|
for _, componentTasks := range [][]*persistencespb.ChasmComponentAttributes_Task{
|
|
componentAttr.PureTasks,
|
|
componentAttr.SideEffectTasks,
|
|
} {
|
|
for _, t := range componentTasks {
|
|
if !reset && t.PhysicalTaskStatus == physicalTaskStatusCreated {
|
|
reset = true
|
|
}
|
|
t.PhysicalTaskStatus = physicalTaskStatusNone
|
|
}
|
|
}
|
|
|
|
return reset
|
|
}
|
|
|
|
func (n *Node) encodedPath() (string, error) {
|
|
return n.pathEncoder.Encode(n, n.path())
|
|
}
|
|
|
|
func (n *Node) path() []string {
|
|
if n.parent == nil {
|
|
return []string{}
|
|
}
|
|
|
|
return append(n.parent.path(), n.nodeName)
|
|
}
|
|
|
|
func (n *Node) findNode(
|
|
path []string,
|
|
) (*Node, bool) {
|
|
if len(path) == 0 {
|
|
return n, true
|
|
}
|
|
|
|
childName := path[0]
|
|
childNode, ok := n.children[childName]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return childNode.findNode(path[1:])
|
|
}
|
|
|
|
func (n *Node) delete(
|
|
currentNodePath []string,
|
|
currentEncodedPathPtr *string,
|
|
) error {
|
|
for childName, childNode := range n.children {
|
|
if err := childNode.delete(append(currentNodePath, childName), nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if n.parent != nil {
|
|
delete(n.parent.children, n.nodeName)
|
|
}
|
|
|
|
var encodedPath string
|
|
if currentEncodedPathPtr == nil {
|
|
var err error
|
|
encodedPath, err = n.pathEncoder.Encode(n, currentNodePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
encodedPath = *currentEncodedPathPtr
|
|
}
|
|
|
|
n.mutation.DeletedNodes[encodedPath] = struct{}{}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsDirty returns true if any node in the tree has been modified,
|
|
// and need to be persisted in DB.
|
|
// The result will be reset to false after a call to CloseTransaction().
|
|
func (n *Node) IsDirty() bool {
|
|
if n.IsStateDirty() {
|
|
return true
|
|
}
|
|
|
|
return len(n.systemMutation.UpdatedNodes) > 0 || len(n.systemMutation.DeletedNodes) > 0
|
|
}
|
|
|
|
// IsStateDirty returns true if any node in the tree has USER DATA modified,
|
|
// which need to be persisted to DB AND replicated to other clusters.
|
|
// The result will be reset to false after a call to CloseTransaction().
|
|
func (n *Node) IsStateDirty() bool {
|
|
if len(n.mutation.UpdatedNodes) > 0 || len(n.mutation.DeletedNodes) > 0 {
|
|
return true
|
|
}
|
|
|
|
return n.isValueNeedSerialize()
|
|
}
|
|
|
|
func (n *Node) IsStale(
|
|
ref ComponentRef,
|
|
) error {
|
|
// The point of this method to access the private entityLastUpdateVT field in componentRef,
|
|
// and avoid exposing it in the public CHASM interface.
|
|
if ref.entityLastUpdateVT == nil {
|
|
return nil
|
|
}
|
|
|
|
return transitionhistory.StalenessCheck(
|
|
n.backend.GetExecutionInfo().TransitionHistory,
|
|
ref.entityLastUpdateVT,
|
|
)
|
|
}
|
|
|
|
func (n *Node) isValueNeedSerialize() bool {
|
|
if n.valueState == valueStateNeedSerialize {
|
|
return true
|
|
}
|
|
|
|
for _, childNode := range n.children {
|
|
if childNode.isValueNeedSerialize() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (n *Node) Terminate(
|
|
request TerminateComponentRequest,
|
|
) error {
|
|
mutableContext := NewMutableContext(context.Background(), n.root())
|
|
component, err := n.Component(mutableContext, ComponentRef{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = component.Terminate(mutableContext, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.terminated = true
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) Archetype() Archetype {
|
|
root := n.root()
|
|
if root.serializedNode == nil {
|
|
// Empty tree
|
|
return ""
|
|
}
|
|
|
|
// Root must have be a component.
|
|
return Archetype(root.serializedNode.Metadata.GetComponentAttributes().Type)
|
|
}
|
|
|
|
func (n *Node) root() *Node {
|
|
if n.parent == nil {
|
|
return n
|
|
}
|
|
return n.parent.root()
|
|
}
|
|
|
|
// isComponentTaskExpired returns true when the task's scheduled time is equal
|
|
// or before the reference time. The caller should also make sure to account
|
|
// for skew between the physical task queue and the database by adjusting
|
|
// referenceTime in advance.
|
|
func isComponentTaskExpired(
|
|
referenceTime time.Time,
|
|
task *persistencespb.ChasmComponentAttributes_Task,
|
|
) bool {
|
|
if task.ScheduledTime == nil {
|
|
return false
|
|
}
|
|
|
|
scheduledTime := task.ScheduledTime.AsTime().Truncate(persistence.ScheduledTaskMinPrecision)
|
|
referenceTime = referenceTime.Truncate(persistence.ScheduledTaskMinPrecision)
|
|
|
|
return !scheduledTime.After(referenceTime)
|
|
}
|
|
|
|
// EachPureTask runs the callback for all expired/runnable pure tasks within the
|
|
// CHASM tree (including invalid tasks). The CHASM tree is left untouched, even
|
|
// if invalid tasks are detected (these are cleaned up as part of transaction
|
|
// close).
|
|
func (n *Node) EachPureTask(
|
|
referenceTime time.Time,
|
|
callback func(executor NodePureTask, taskAttributes TaskAttributes, task any) error,
|
|
) error {
|
|
ctx := NewContext(context.Background(), n)
|
|
|
|
// Walk the tree to find all runnable tasks.
|
|
for _, node := range n.andAllChildren() {
|
|
// Skip nodes that aren't serialized yet.
|
|
if node.serializedNode == nil || node.serializedNode.Metadata == nil {
|
|
continue
|
|
}
|
|
|
|
componentAttr := node.serializedNode.Metadata.GetComponentAttributes()
|
|
// Skip nodes that aren't components.
|
|
if componentAttr == nil {
|
|
continue
|
|
}
|
|
|
|
// Hydrate nodes before the task validator is called.
|
|
err := node.prepareComponentValue(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, task := range componentAttr.GetPureTasks() {
|
|
if !isComponentTaskExpired(referenceTime, task) {
|
|
// Pure tasks are stored in-order, so we can skip scanning the rest once we hit
|
|
// an unexpired task deadline.
|
|
break
|
|
}
|
|
|
|
taskValue, err := node.deserializeComponentTask(task)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
taskAttributes := TaskAttributes{
|
|
ScheduledTime: task.ScheduledTime.AsTime(),
|
|
Destination: task.Destination,
|
|
}
|
|
|
|
if err = callback(node, taskAttributes, taskValue); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func newNode(
|
|
base *nodeBase,
|
|
parent *Node,
|
|
nodeName string,
|
|
) *Node {
|
|
return &Node{
|
|
nodeBase: base,
|
|
parent: parent,
|
|
children: make(map[string]*Node),
|
|
nodeName: nodeName,
|
|
}
|
|
}
|
|
|
|
func compareSideEffectTasks(a, b *persistencespb.ChasmComponentAttributes_Task) int {
|
|
if cmpResult := transitionhistory.Compare(a.VersionedTransition, b.VersionedTransition); cmpResult != 0 {
|
|
return cmpResult
|
|
}
|
|
return cmp.Compare(a.VersionedTransitionOffset, b.VersionedTransitionOffset)
|
|
}
|
|
|
|
func comparePureTasks(a, b *persistencespb.ChasmComponentAttributes_Task) int {
|
|
if cmpResult := a.ScheduledTime.AsTime().Compare(b.ScheduledTime.AsTime()); cmpResult != 0 {
|
|
return cmpResult
|
|
}
|
|
|
|
return compareSideEffectTasks(a, b)
|
|
}
|
|
|
|
func carryOverTaskStatus(
|
|
sourceTasks, targetTasks []*persistencespb.ChasmComponentAttributes_Task,
|
|
compareFn func(a, b *persistencespb.ChasmComponentAttributes_Task) int,
|
|
) {
|
|
sourceIdx, targetIdx := 0, 0
|
|
for sourceIdx < len(sourceTasks) && targetIdx < len(targetTasks) {
|
|
sourceTask := sourceTasks[sourceIdx]
|
|
targetTask := targetTasks[targetIdx]
|
|
|
|
switch compareFn(sourceTask, targetTask) {
|
|
case 0:
|
|
// Task match, carry over status.
|
|
targetTask.PhysicalTaskStatus = sourceTask.PhysicalTaskStatus
|
|
sourceIdx++
|
|
targetIdx++
|
|
case -1:
|
|
// Source task has a smaller key, meaning the task has been deleted.
|
|
// Move on to the next source task.
|
|
sourceIdx++
|
|
case 1:
|
|
// Source task has a larger key, meaning there's a new task inserted.
|
|
// Sanitize incoming task status.
|
|
targetTask.PhysicalTaskStatus = physicalTaskStatusNone
|
|
targetIdx++
|
|
}
|
|
}
|
|
|
|
// Sanitize incoming task status for remaining tasks.
|
|
for ; targetIdx < len(targetTasks); targetIdx++ {
|
|
targetTasks[targetIdx].PhysicalTaskStatus = physicalTaskStatusNone
|
|
}
|
|
}
|
|
|
|
func taskCategory(
|
|
task *persistencespb.ChasmComponentAttributes_Task,
|
|
) (tasks.Category, error) {
|
|
if task.Type == visibilityTaskFqType {
|
|
return tasks.CategoryVisibility, nil
|
|
}
|
|
|
|
isImmediate := task.ScheduledTime == nil || task.ScheduledTime.AsTime().Equal(TaskScheduledTimeImmediate)
|
|
|
|
if task.Destination != "" {
|
|
if !isImmediate {
|
|
return tasks.Category{}, serviceerror.NewInternalf(
|
|
"Task cannot have both destination and scheduled time set, destination: %v, scheduled time: %v", task.Destination, task.ScheduledTime.AsTime(),
|
|
)
|
|
}
|
|
return tasks.CategoryOutbound, nil
|
|
}
|
|
|
|
if isImmediate {
|
|
return tasks.CategoryTransfer, nil
|
|
}
|
|
return tasks.CategoryTimer, nil
|
|
}
|
|
|
|
func deserializeTask(
|
|
registrableTask *RegistrableTask,
|
|
taskBlob *commonpb.DataBlob,
|
|
) (taskValue reflect.Value, retErr error) {
|
|
if registrableTask.goType.AssignableTo(protoMessageT) {
|
|
taskValue, err := unmarshalProto(taskBlob, registrableTask.goType)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
return taskValue, nil
|
|
}
|
|
|
|
taskGoType := registrableTask.goType
|
|
if taskGoType.Kind() == reflect.Ptr {
|
|
taskGoType = taskGoType.Elem()
|
|
}
|
|
taskValue = reflect.New(taskGoType)
|
|
|
|
// At this point taskGoType is guaranteed to be a struct and
|
|
// taskValue is a pointer to struct.
|
|
|
|
defer func() {
|
|
if retErr == nil && registrableTask.goType.Kind() == reflect.Struct {
|
|
taskValue = taskValue.Elem()
|
|
}
|
|
}()
|
|
|
|
if taskGoType.NumField() == 0 {
|
|
return taskValue, nil
|
|
}
|
|
|
|
// TODO: consider pre-calculating the proto field num when registring the task type.
|
|
|
|
protoMessageFound := false
|
|
for i := 0; i < taskGoType.NumField(); i++ {
|
|
fieldV := taskValue.Elem().Field(i)
|
|
fieldT := taskGoType.Field(i).Type
|
|
if !fieldT.AssignableTo(protoMessageT) {
|
|
continue
|
|
}
|
|
|
|
if protoMessageFound {
|
|
return reflect.Value{}, serviceerror.NewInternal("only one proto field allowed in task struct")
|
|
}
|
|
protoMessageFound = true
|
|
|
|
value, err := unmarshalProto(taskBlob, fieldT)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
|
|
fieldV.Set(value)
|
|
}
|
|
|
|
return taskValue, nil
|
|
}
|
|
|
|
func serializeTask(
|
|
registrableTask *RegistrableTask,
|
|
task any,
|
|
) (*commonpb.DataBlob, error) {
|
|
protoValue, ok := task.(proto.Message)
|
|
if ok {
|
|
return serialization.ProtoEncode(protoValue)
|
|
}
|
|
|
|
taskGoType := registrableTask.goType
|
|
taskValue := reflect.ValueOf(task)
|
|
|
|
// Handle pointer to struct.
|
|
if taskGoType.Kind() == reflect.Ptr {
|
|
taskGoType = taskGoType.Elem()
|
|
taskValue = taskValue.Elem()
|
|
}
|
|
|
|
// Handle empty task struct.
|
|
if taskGoType.NumField() == 0 {
|
|
return &commonpb.DataBlob{
|
|
Data: nil,
|
|
EncodingType: enumspb.ENCODING_TYPE_PROTO3,
|
|
}, nil
|
|
}
|
|
|
|
// TODO: consider pre-calculating the proto field num when registring the task type.
|
|
|
|
var blob *commonpb.DataBlob
|
|
protoMessageFound := false
|
|
for i := 0; i < taskGoType.NumField(); i++ {
|
|
fieldV := taskValue.Field(i)
|
|
if !fieldV.Type().AssignableTo(protoMessageT) {
|
|
continue
|
|
}
|
|
|
|
if protoMessageFound {
|
|
return nil, serviceerror.NewInternalf("only one proto field allowed in task struct of type: %v", taskGoType.String())
|
|
}
|
|
protoMessageFound = true
|
|
|
|
var err error
|
|
blob, err = serialization.ProtoEncode(fieldV.Interface().(proto.Message))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if !protoMessageFound {
|
|
return nil, serviceerror.NewInternal("no proto field found in task struct")
|
|
}
|
|
|
|
return blob, nil
|
|
}
|
|
|
|
// ExecutePureTask validates and then executes the given taskInstance against the
|
|
// node's component. Executing an invalid task is a no-op (no error returned).
|
|
func (n *Node) ExecutePureTask(
|
|
baseCtx context.Context,
|
|
taskAttributes TaskAttributes,
|
|
taskInstance any,
|
|
) error {
|
|
registrableTask, ok := n.registry.taskFor(taskInstance)
|
|
if !ok {
|
|
return fmt.Errorf("unknown task type for task instance goType '%s'", reflect.TypeOf(taskInstance).Name())
|
|
}
|
|
|
|
if !registrableTask.isPureTask {
|
|
return fmt.Errorf("ExecutePureTask called on a SideEffect task '%s'", registrableTask.fqType())
|
|
}
|
|
|
|
ctx := NewMutableContext(
|
|
newContextWithOperationIntent(baseCtx, OperationIntentProgress),
|
|
n,
|
|
)
|
|
|
|
// Ensure this node's component value is hydrated before execution. Component
|
|
// will also check access rules.
|
|
component, err := n.Component(ctx, ComponentRef{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Run the task's registered value before execution.
|
|
valid, err := n.validateTask(ctx, taskAttributes, taskInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !valid {
|
|
return nil
|
|
}
|
|
|
|
result := registrableTask.executeFn.Call([]reflect.Value{
|
|
reflect.ValueOf(ctx),
|
|
reflect.ValueOf(component),
|
|
reflect.ValueOf(taskAttributes),
|
|
reflect.ValueOf(taskInstance),
|
|
})
|
|
if !result[0].IsNil() {
|
|
//nolint:revive // type cast result is unchecked
|
|
return result[0].Interface().(error)
|
|
}
|
|
|
|
// TODO - a task validator must succeed validation after a task executes
|
|
// successfully (without error), otherwise it will generate an infinite loop.
|
|
// Check for this case by marking the in-memory task as having executed, which the
|
|
// CloseTransaction method will check against.
|
|
//
|
|
// See: https://github.com/temporalio/temporal/pull/7701#discussion_r2072026993
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidatePureTask runs a pure task's associated validator, returning true
|
|
// if the task is valid. Intended for use by standby executors as part of
|
|
// EachPureTask's callback.
|
|
// This method assumes the node's value has already been prepared (hydrated).
|
|
func (n *Node) ValidatePureTask(
|
|
ctx context.Context,
|
|
taskAttributes TaskAttributes,
|
|
taskInstance any,
|
|
) (bool, error) {
|
|
validateCtx := NewContext(ctx, n)
|
|
return n.validateTask(validateCtx, taskAttributes, taskInstance)
|
|
}
|
|
|
|
// ValidateSideEffectTask runs a side effect task's associated validator,
|
|
// returning the deserialized task instance if the task is valid. Intended for
|
|
// use by standby executors.
|
|
//
|
|
// If validation succeeds but the task is invalid, nil is returned to signify the
|
|
// task can be skipped/deleted.
|
|
//
|
|
// If validation fails, that error is returned.
|
|
func (n *Node) ValidateSideEffectTask(
|
|
ctx context.Context,
|
|
taskAttributes TaskAttributes,
|
|
taskInfo *persistencespb.ChasmTaskInfo,
|
|
) (any, error) {
|
|
taskType := taskInfo.Type
|
|
registrableTask, ok := n.registry.task(taskType)
|
|
if !ok {
|
|
return nil, serviceerror.NewInternalf("unknown task type '%s'", taskType)
|
|
}
|
|
|
|
if registrableTask.isPureTask {
|
|
return nil, serviceerror.NewInternalf("ValidateSideEffectTask called on a Pure task '%s'", taskType)
|
|
}
|
|
|
|
nodePath, err := n.pathEncoder.Decode(taskInfo.Path)
|
|
if err != nil {
|
|
return nil, serviceerror.NewInternalf("failed to decode path '%s'", taskInfo.Path)
|
|
}
|
|
|
|
node, ok := n.findNode(nodePath)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
// node.serializedNode should always be available when running a side effect task.
|
|
if transitionhistory.Compare(
|
|
taskInfo.ComponentInitialVersionedTransition,
|
|
node.serializedNode.Metadata.InitialVersionedTransition,
|
|
) != 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Component must be hydrated before the task's validator is called.
|
|
validateCtx := NewContext(ctx, n)
|
|
if err := node.prepareComponentValue(validateCtx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO - cache deserialized task
|
|
taskValue, err := deserializeTask(registrableTask, taskInfo.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
taskInstance := taskValue.Interface()
|
|
|
|
valid, err := node.validateTask(validateCtx, taskAttributes, taskInstance)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !valid {
|
|
return nil, nil
|
|
}
|
|
|
|
return taskInstance, nil
|
|
}
|
|
|
|
// ExecuteSideEffectTask executes the given ChasmTask on its associated node
|
|
// without holding the entity lock.
|
|
//
|
|
// WARNING: This method *must not* access the node's properties without first
|
|
// locking the entity.
|
|
//
|
|
// ctx should have a CHASM engine already set.
|
|
func (n *Node) ExecuteSideEffectTask(
|
|
ctx context.Context,
|
|
registry *Registry,
|
|
entityKey EntityKey,
|
|
taskAttributes TaskAttributes,
|
|
taskInfo *persistencespb.ChasmTaskInfo,
|
|
validate func(NodeBackend, Context, Component) error,
|
|
) error {
|
|
if engineFromContext(ctx) == nil {
|
|
return serviceerror.NewInternal("no CHASM engine set on context")
|
|
}
|
|
|
|
taskType := taskInfo.Type
|
|
registrableTask, ok := registry.task(taskType)
|
|
if !ok {
|
|
return serviceerror.NewInternalf("unknown task type '%s'", taskType)
|
|
}
|
|
|
|
if registrableTask.isPureTask {
|
|
return serviceerror.NewInternalf("ExecuteSideEffectTask called on a Pure task '%s'", taskType)
|
|
}
|
|
|
|
// TODO - update ComponentRef to use the encoded path, and then leave decoding
|
|
// until access/dereference time.
|
|
path, err := n.pathEncoder.Decode(taskInfo.Path)
|
|
if err != nil {
|
|
return serviceerror.NewInternalf("failed to decode path '%s'", taskInfo.Path)
|
|
}
|
|
|
|
taskValue, err := deserializeTask(registrableTask, taskInfo.Data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ref := ComponentRef{
|
|
EntityKey: entityKey,
|
|
archetype: n.Archetype(),
|
|
entityLastUpdateVT: taskInfo.ComponentLastUpdateVersionedTransition,
|
|
componentPath: path,
|
|
componentInitialVT: taskInfo.ComponentInitialVersionedTransition,
|
|
|
|
// Validate the Ref only once it is accessed by the task's executor.
|
|
validationFn: makeValidationFn(registrableTask, validate, taskAttributes, taskValue),
|
|
}
|
|
|
|
ctx = newContextWithOperationIntent(ctx, OperationIntentProgress)
|
|
|
|
result := registrableTask.executeFn.Call([]reflect.Value{
|
|
reflect.ValueOf(ctx),
|
|
reflect.ValueOf(ref),
|
|
reflect.ValueOf(taskAttributes),
|
|
taskValue,
|
|
})
|
|
if !result[0].IsNil() {
|
|
//nolint:revive // type cast result is unchecked
|
|
return result[0].Interface().(error)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) ComponentByPath(
|
|
chasmContext Context,
|
|
encodedPath string,
|
|
) (Component, error) {
|
|
nodePath, err := n.pathEncoder.Decode(encodedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node, ok := n.findNode(nodePath)
|
|
if !ok {
|
|
return nil, errComponentNotFound
|
|
}
|
|
|
|
if err := node.prepareComponentValue(chasmContext); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
componentValue, ok := node.value.(Component)
|
|
if !ok {
|
|
return nil, serviceerror.NewInternalf(
|
|
"component value is not of type Component: %v", reflect.TypeOf(node.value),
|
|
)
|
|
}
|
|
|
|
return componentValue, nil
|
|
}
|
|
|
|
// makeValidationFn adapts the TaskValidator interface to the ComponentRef's
|
|
// validation callback format. Returns a validation function that wraps the
|
|
// given validation callback to be called before the RegistrableTask's registered
|
|
// validator callback. Intended for use to validate mutable state at access time.
|
|
func makeValidationFn(
|
|
registrableTask *RegistrableTask,
|
|
validate func(NodeBackend, Context, Component) error,
|
|
taskAttributes TaskAttributes,
|
|
taskValue reflect.Value,
|
|
) func(NodeBackend, Context, Component) error {
|
|
return func(backend NodeBackend, ctx Context, component Component) error {
|
|
// Call the provided validation callback.
|
|
err := validate(backend, ctx, component)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Call the TaskValidator interface.
|
|
result := registrableTask.validateFn.Call([]reflect.Value{
|
|
reflect.ValueOf(ctx),
|
|
reflect.ValueOf(component),
|
|
reflect.ValueOf(taskAttributes),
|
|
taskValue,
|
|
})
|
|
|
|
// Handle err.
|
|
if !result[1].IsNil() {
|
|
//nolint:revive // type cast result is unchecked
|
|
return result[1].Interface().(error)
|
|
}
|
|
|
|
// Handle bool result.
|
|
if !result[0].Bool() {
|
|
return errTaskNotValid
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|