mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-30 18:41:49 -07:00
## What changed? - Previously when creating pointers within a CHASM tree, the pointed-to component had to already exist within the tree, as the pointer was resolved immediately. This was cumbersome in some common cases like setting up an initial tree of CHASM components (such as setting up a scheduler tree), where ideally, all components would be created and linked up within the first transaction. - A new field type, `fieldTypeDeferredPointer`, has been introduced. This field stores the pointer value directly, and resolves the value to the path of the component within the tree during `CloseTransaction` (or fails if the pointer is dangling). ## How did you test it? - [ ] built - [ ] run locally and tested manually - [ ] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s)
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.ProtoEncodeBlob(field.val.Interface().(proto.Message), enumspb.ENCODING_TYPE_PROTO3); 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.ProtoEncodeBlob(protoValue, enumspb.ENCODING_TYPE_PROTO3); 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.ProtoDecodeBlob(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.ProtoEncodeBlob(protoValue, enumspb.ENCODING_TYPE_PROTO3)
|
|
}
|
|
|
|
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.ProtoEncodeBlob(fieldV.Interface().(proto.Message), enumspb.ENCODING_TYPE_PROTO3)
|
|
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
|
|
}
|
|
}
|