Skip to content

Commit 2e7b2c7

Browse files
authored
CHASM: Add ParentPtr field (#8672)
## What changed? - Add CHASM ParentPtr ## Why? - Current implementation of CHASM pointer is a separate physical node. But in most of the use case we have, we only need to point to the parent component which can be done entirely in memory without persisting anything. ## How did you test it? - [x] built - [ ] run locally and tested manually - [ ] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s)
1 parent a130624 commit 2e7b2c7

File tree

5 files changed

+207
-5
lines changed

5 files changed

+207
-5
lines changed

chasm/fields_iterator.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import (
99
)
1010

1111
const (
12-
chasmFieldTypePrefix = "chasm.Field["
13-
chasmMapTypePrefix = "chasm.Map["
14-
chasmMSPointerType = "chasm.MSPointer"
12+
chasmFieldTypePrefix = "chasm.Field["
13+
chasmMapTypePrefix = "chasm.Map["
14+
chasmMSPointerType = "chasm.MSPointer"
15+
chasmParentPointerTypePrefix = "chasm.ParentPtr["
1516

1617
fieldNameTag = "name"
1718
)
@@ -24,6 +25,7 @@ const (
2425
fieldKindSubField
2526
fieldKindSubMap
2627
fieldKindMutableState
28+
fieldKindParentPtr
2729
)
2830

2931
type fieldInfo struct {
@@ -62,7 +64,10 @@ func fieldsOf(valueV reflect.Value) iter.Seq[fieldInfo] {
6264
prefix := genericTypePrefix(fieldT)
6365
if strings.HasPrefix(prefix, "*") {
6466
switch prefix[1:] {
65-
case chasmFieldTypePrefix, chasmMapTypePrefix, chasmMSPointerType:
67+
case chasmFieldTypePrefix,
68+
chasmMapTypePrefix,
69+
chasmMSPointerType,
70+
chasmParentPointerTypePrefix:
6671
fieldErr = serviceerror.NewInternalf("%s.%s: CHASM fields must not be pointers", valueT, fieldN)
6772
default:
6873
continue
@@ -75,6 +80,8 @@ func fieldsOf(valueV reflect.Value) iter.Seq[fieldInfo] {
7580
fieldK = fieldKindSubMap
7681
case chasmMSPointerType:
7782
fieldK = fieldKindMutableState
83+
case chasmParentPointerTypePrefix:
84+
fieldK = fieldKindParentPtr
7885
default:
7986
continue // Skip non-CHASM fields.
8087
}
@@ -113,7 +120,10 @@ func unmanagedFieldsOf(valueT reflect.Type) iter.Seq[fieldInfo] {
113120
fieldN := fieldName(valueT.Field(i))
114121
prefix := genericTypePrefix(fieldT)
115122
switch prefix {
116-
case chasmFieldTypePrefix, chasmMapTypePrefix, chasmMSPointerType:
123+
case chasmFieldTypePrefix,
124+
chasmMapTypePrefix,
125+
chasmMSPointerType,
126+
chasmParentPointerTypePrefix:
117127
continue // Skip CHASM fields.
118128
default:
119129
if !yield(fieldInfo{typ: fieldT, name: fieldN}) {

chasm/parent_pointer.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package chasm
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"go.temporal.io/api/serviceerror"
8+
"go.temporal.io/server/common/softassert"
9+
)
10+
11+
const (
12+
parentPtrInternalFieldName = "Internal"
13+
)
14+
15+
// ParentPtr is a in-memory pointer to the parent component of a CHASM component.
16+
//
17+
// CHASM map is not a component, so if a component is inside a map, its ParentPtr
18+
// will point to the nearest ancestor component that is not a map.
19+
//
20+
// ParentPtr is only initialized and available for use **after** the transition that
21+
// creates the component using ParentPtr is completed.
22+
type ParentPtr[T Component] struct {
23+
// Exporting this field as this generic struct needs to be created via reflection,
24+
// and reflection can't set private fields.
25+
Internal parentPtrInternal
26+
}
27+
28+
type parentPtrInternal struct {
29+
// Storing currentNode instead of parent component Node here so that
30+
// we can differentiate between root node and non-initialized ParentPtr.
31+
currentNode *Node
32+
}
33+
34+
// Get returns the parent component, deserializing it if necessary.
35+
// Panics rather than returning an error, as errors are supposed to be handled by the framework as opposed to the
36+
// application.
37+
func (p ParentPtr[T]) Get(chasmContext Context) T {
38+
vT, ok := p.TryGet(chasmContext)
39+
if !ok {
40+
// nolint:forbidigo // Panic is intended here for framework error handling.
41+
panic(serviceerror.NewInternal("expect parent component value but got nil"))
42+
}
43+
return vT
44+
}
45+
46+
// TryGet returns the parent component and a boolean indicating if the value was found,
47+
// deserializing if necessary.
48+
// Panics rather than returning an error, as errors are supposed to be handled by the framework as opposed to the
49+
// application.
50+
func (p ParentPtr[T]) TryGet(chasmContext Context) (T, bool) {
51+
var nilT T
52+
if p.Internal.currentNode == nil {
53+
// nolint:forbidigo // Panic is intended here for framework error handling.
54+
panic(serviceerror.NewInternal("parent pointer not initialized yet"))
55+
}
56+
57+
parent := p.Internal.currentNode.parent
58+
if parent == nil {
59+
return nilT, false
60+
}
61+
62+
for parent.isMap() {
63+
parent = parent.parent
64+
if parent == nil {
65+
encodedPath, _ := p.Internal.currentNode.getEncodedPath()
66+
// nolint:forbidigo // Panic is intended here for framework error handling.
67+
panic(softassert.UnexpectedInternalErr(
68+
p.Internal.currentNode.logger,
69+
"unable to find parent component for CHASM component inside a map",
70+
fmt.Errorf("child node name: %s", encodedPath),
71+
))
72+
}
73+
}
74+
75+
if !parent.isComponent() {
76+
// nolint:forbidigo // Panic is intended here for framework error handling.
77+
panic(softassert.UnexpectedInternalErr(
78+
parent.logger,
79+
"unexpected CHASM node that has a child component",
80+
fmt.Errorf("node %s, node metadata: %s",
81+
parent.nodeName,
82+
parent.serializedNode.GetMetadata().String(),
83+
),
84+
))
85+
}
86+
87+
if err := parent.prepareComponentValue(chasmContext); err != nil {
88+
// nolint:forbidigo // Panic is intended here for framework error handling.
89+
panic(err)
90+
}
91+
92+
if parent.value == nil {
93+
return nilT, false
94+
}
95+
96+
vT, isT := parent.value.(T)
97+
if !isT {
98+
// nolint:forbidigo // Panic is intended here for framework error handling.
99+
panic(serviceerror.NewInternalf("parent component value doesn't implement %s", reflect.TypeFor[T]().Name()))
100+
}
101+
return vT, true
102+
}

chasm/test_component_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type (
3131
SubComponentInterfacePointer Field[Component]
3232

3333
MSPointer MSPointer
34+
ParentPtr ParentPtr[*TestComponent]
3435

3536
Visibility Field[*Visibility]
3637
}
@@ -44,12 +45,16 @@ type (
4445
SubData11 Field[*protoMessageType] // Random proto message.
4546
SubComponent2Pointer Field[*TestSubComponent2]
4647
DataPointer Field[*protoMessageType]
48+
49+
ParentPtr ParentPtr[*TestComponent]
4750
}
4851

4952
TestSubComponent11 struct {
5053
UnimplementedComponent
5154

5255
SubComponent11Data *protoMessageType
56+
57+
ParentPtr ParentPtr[*TestSubComponent1]
5358
}
5459

5560
TestSubComponent2 struct {

chasm/tree.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,10 @@ func (n *Node) isComponent() bool {
538538
return n.serializedNode.GetMetadata().GetComponentAttributes() != nil
539539
}
540540

541+
func (n *Node) isMap() bool {
542+
return n.serializedNode.GetMetadata().GetCollectionAttributes() != nil
543+
}
544+
541545
func (n *Node) fieldType() fieldType {
542546
if n.serializedNode.GetMetadata().GetComponentAttributes() != nil {
543547
return fieldTypeComponent
@@ -757,6 +761,19 @@ func (n *Node) syncSubComponents() error {
757761
if keepChild {
758762
childrenToKeep[field.name] = struct{}{}
759763
}
764+
case fieldKindParentPtr:
765+
internalField := field.val.FieldByName(parentPtrInternalFieldName)
766+
internal, ok := internalField.Interface().(parentPtrInternal)
767+
if !ok {
768+
return softassert.UnexpectedInternalErr(
769+
n.logger,
770+
"CHASM parent pointer's internal field is not of parentPtrInternal type",
771+
fmt.Errorf("node %s, actual type: %T", n.nodeName, internalField.Interface()))
772+
}
773+
if internal.currentNode == nil || internal.currentNode != n {
774+
internal.currentNode = n
775+
internalField.Set(reflect.ValueOf(internal))
776+
}
760777
case fieldKindSubMap:
761778
if field.val.IsNil() {
762779
// If Map field is nil then delete all collection items nodes and collection node itself.
@@ -1141,6 +1158,12 @@ func (n *Node) deserializeComponentNode(
11411158
}
11421159
case fieldKindMutableState:
11431160
field.val.Set(reflect.ValueOf(NewMSPointer(n.backend)))
1161+
case fieldKindParentPtr:
1162+
parentPtrV := reflect.New(field.typ).Elem()
1163+
parentPtrV.FieldByName(parentPtrInternalFieldName).Set(reflect.ValueOf(parentPtrInternal{
1164+
currentNode: n,
1165+
}))
1166+
field.val.Set(parentPtrV)
11441167
}
11451168
}
11461169

chasm/tree_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,68 @@ func (s *nodeSuite) TestPointerAttributes() {
513513
})
514514
}
515515

516+
func (s *nodeSuite) TestParentPointer_InMemory() {
517+
node := s.testComponentTree()
518+
519+
s.assertParentPointer(node)
520+
521+
// Additionally also test parentPtr for components inside a map.
522+
523+
mutableContext := NewMutableContext(context.Background(), node)
524+
component, err := node.Component(mutableContext, ComponentRef{})
525+
s.NoError(err)
526+
testComponent := component.(*TestComponent)
527+
528+
mapSubComponent1 := &TestSubComponent1{}
529+
// Try using the testComponent we get from the ParentPtr for the mutation.
530+
testComponent.SubComponents = Map[string, *TestSubComponent1]{
531+
"mapSubComponent1": NewComponentField(mutableContext, mapSubComponent1),
532+
}
533+
534+
s.Panics(func() {
535+
_ = mapSubComponent1.ParentPtr.Get(mutableContext)
536+
})
537+
538+
// Sync structure initializes the parent pointer
539+
err = node.syncSubComponents()
540+
s.NoError(err)
541+
542+
testComponentFromPtr := mapSubComponent1.ParentPtr.Get(mutableContext)
543+
// Asserting they actually point to the same testComponent object.
544+
s.Same(testComponent, testComponentFromPtr)
545+
}
546+
547+
func (s *nodeSuite) TestParentPointer_FromDB() {
548+
serializedNodes := testComponentSerializedNodes()
549+
550+
node, err := s.newTestTree(serializedNodes)
551+
s.NoError(err)
552+
553+
s.assertParentPointer(node)
554+
}
555+
556+
func (s *nodeSuite) assertParentPointer(testComponentNode *Node) {
557+
chasmContext := NewContext(context.Background(), testComponentNode)
558+
component, err := testComponentNode.Component(chasmContext, ComponentRef{})
559+
s.NoError(err)
560+
testComponent := component.(*TestComponent)
561+
562+
_, found := testComponent.ParentPtr.TryGet(chasmContext)
563+
s.False(found)
564+
565+
subComponent1, err := testComponent.SubComponent1.Get(chasmContext)
566+
s.NoError(err)
567+
testComponentFromPtr := subComponent1.ParentPtr.Get(chasmContext)
568+
// Asserting they actually point to the same testComponent object.
569+
s.Same(testComponent, testComponentFromPtr)
570+
571+
subComponent11, err := subComponent1.SubComponent11.Get(chasmContext)
572+
s.NoError(err)
573+
testSubComponent1FromPtr := subComponent11.ParentPtr.Get(chasmContext)
574+
// Asserting they actually point to the same testSubComponent1 object.
575+
s.Same(subComponent1, testSubComponent1FromPtr)
576+
}
577+
516578
func (s *nodeSuite) TestSyncSubComponents_DeleteLeafNode() {
517579
node := s.testComponentTree()
518580

0 commit comments

Comments
 (0)