Files
temporal/chasm/skip_persistence_test.go
Alan Wu 43c32ac9f7 Add dynamic config to flip skip persistence optimization (#11318)
## What changed?
Add dynamic config to flip skip persistence optimization.

## Why?
Allow backwards compatible rollout of skip persistence flag.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [ ] added new functional test(s)
2026-07-28 01:27:11 +00:00

219 lines
8.0 KiB
Go

package chasm
import (
"context"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/proto"
)
func (s *nodeSuite) minimalTestComponent() *TestComponent {
return &TestComponent{
ComponentData: &persistencespb.WorkflowExecutionState{RunId: "initial-run-id"},
MSPointer: NewMSPointer(s.nodeBackend),
}
}
func (s *nodeSuite) newSkipPersistenceTestTree(
serializedNodes map[string]*persistencespb.ChasmNode,
enabled func() bool,
) *Node {
s.nodeBackend.HandleChasmSkipPersistenceEnabled = enabled
if len(serializedNodes) == 0 {
return NewEmptyTree(
s.registry,
s.timeSource,
s.nodeBackend,
s.nodePathEncoder,
s.logger,
s.metricsHandler,
)
}
root, err := NewTreeFromDB(
serializedNodes,
s.registry,
s.timeSource,
s.nodeBackend,
s.nodePathEncoder,
s.logger,
s.metricsHandler,
)
s.NoError(err)
return root
}
func (s *nodeSuite) TestSkipPersistenceIfClean_NewNode() {
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 1 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode := s.newSkipPersistenceTestTree(nil, func() bool { return true })
s.NoError(rootNode.SetRootComponent(s.minimalTestComponent()))
mutation, err := rootNode.CloseTransaction()
s.NoError(err)
s.Contains(mutation.UpdatedNodes, "", "brand-new node must always be persisted")
s.NotNil(mutation.UpdatedNodes[""].GetData(), "data blob must be present")
}
func (s *nodeSuite) TestSkipPersistenceIfClean_LoadedUnmodified() {
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 1 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode := s.newSkipPersistenceTestTree(nil, func() bool { return true })
s.NoError(rootNode.SetRootComponent(s.minimalTestComponent()))
firstMutation, err := rootNode.CloseTransaction()
s.NoError(err)
s.Contains(firstMutation.UpdatedNodes, "", "new node must be in UpdatedNodes")
persistedNodes := common.CloneProtoMap(firstMutation.UpdatedNodes)
originalTransition := proto.Clone(
persistedNodes[""].GetMetadata().GetLastUpdateVersionedTransition(),
).(*persistencespb.VersionedTransition)
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 2 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode2 := s.newSkipPersistenceTestTree(persistedNodes, func() bool { return true })
ctx := NewMutableContext(context.Background(), rootNode2)
_, err = rootNode2.Component(ctx, ComponentRef{})
s.NoError(err)
secondMutation, err := rootNode2.CloseTransaction()
s.NoError(err)
s.NotContains(secondMutation.UpdatedNodes, "", "unchanged node must not be in UpdatedNodes")
s.Equal(
originalTransition.TransitionCount,
rootNode2.serializedNode.GetMetadata().GetLastUpdateVersionedTransition().GetTransitionCount(),
"LastUpdateVersionedTransition must not be bumped for an unchanged node",
)
}
func (s *nodeSuite) TestSkipPersistenceIfClean_LoadedModified() {
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 1 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode := s.newSkipPersistenceTestTree(nil, func() bool { return true })
s.NoError(rootNode.SetRootComponent(s.minimalTestComponent()))
firstMutation, err := rootNode.CloseTransaction()
s.NoError(err)
persistedNodes := common.CloneProtoMap(firstMutation.UpdatedNodes)
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 2 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode2 := s.newSkipPersistenceTestTree(persistedNodes, func() bool { return true })
ctx := NewMutableContext(context.Background(), rootNode2)
component, err := rootNode2.Component(ctx, ComponentRef{})
s.NoError(err)
component.(*TestComponent).ComponentData.RunId = "modified-run-id"
secondMutation, err := rootNode2.CloseTransaction()
s.NoError(err)
s.Contains(secondMutation.UpdatedNodes, "", "modified node must be in UpdatedNodes")
s.Equal(
int64(2),
secondMutation.UpdatedNodes[""].GetMetadata().GetLastUpdateVersionedTransition().GetTransitionCount(),
"LastUpdateVersionedTransition must be bumped for a modified node",
)
}
func (s *nodeSuite) TestSkipPersistenceIfClean_WithNewTask() {
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 1 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode := s.newSkipPersistenceTestTree(nil, func() bool { return true })
s.NoError(rootNode.SetRootComponent(s.minimalTestComponent()))
firstMutation, err := rootNode.CloseTransaction()
s.NoError(err)
persistedNodes := common.CloneProtoMap(firstMutation.UpdatedNodes)
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 2 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode2 := s.newSkipPersistenceTestTree(persistedNodes, func() bool { return true })
ctx := NewMutableContext(context.Background(), rootNode2)
component, err := rootNode2.Component(ctx, ComponentRef{})
s.NoError(err)
s.testLibrary.mockSideEffectTaskHandler.EXPECT().
Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1)
ctx.AddTask(component, TaskAttributes{}, &TestSideEffectTask{Data: []byte("task-payload")})
secondMutation, err := rootNode2.CloseTransaction()
s.NoError(err)
s.Contains(secondMutation.UpdatedNodes, "", "node with a new task must be in UpdatedNodes even if data is unchanged")
componentAttr := secondMutation.UpdatedNodes[""].GetMetadata().GetComponentAttributes()
s.Len(componentAttr.SideEffectTasks, 1, "side-effect task must be written to the persisted node")
}
func (s *nodeSuite) TestSkipPersistenceIfClean_DeleteUnpersistedNode() {
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 1 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode := s.newSkipPersistenceTestTree(nil, func() bool { return true })
s.NoError(rootNode.SetRootComponent(&TestComponent{
ComponentData: &persistencespb.WorkflowExecutionState{RunId: "root"},
SubComponent1: NewComponentField(nil, &TestSubComponent1{
SubComponent1Data: &persistencespb.WorkflowExecutionState{RunId: "created-then-deleted"},
}),
MSPointer: NewMSPointer(s.nodeBackend),
}))
component := rootNode.value.(*TestComponent)
component.SubComponent1 = NewEmptyField[*TestSubComponent1]()
mutation, err := rootNode.CloseTransaction()
s.NoError(err)
s.Contains(mutation.UpdatedNodes, "", "root still needs initial persistence")
s.Empty(mutation.DeletedNodes, "node created and deleted before first persistence must not emit a tombstone")
}
func (s *nodeSuite) TestSkipPersistenceIfClean_DynamicConfig() {
s.nodeBackend.HandleNextTransitionCount = func() int64 { return 1 }
s.nodeBackend.HandleGetCurrentVersion = func() int64 { return 1 }
rootNode := s.newSkipPersistenceTestTree(nil, func() bool { return false })
s.NoError(rootNode.SetRootComponent(s.minimalTestComponent()))
firstMutation, err := rootNode.CloseTransaction()
s.NoError(err)
persistedNodes := common.CloneProtoMap(firstMutation.UpdatedNodes)
enabled := false
nextTransitionCount := int64(2)
s.nodeBackend.HandleNextTransitionCount = func() int64 { return nextTransitionCount }
rootNode2 := s.newSkipPersistenceTestTree(persistedNodes, func() bool { return enabled })
ctx := NewMutableContext(context.Background(), rootNode2)
_, err = rootNode2.Component(ctx, ComponentRef{})
s.NoError(err)
mutation, err := rootNode2.CloseTransaction()
s.NoError(err)
s.Contains(mutation.UpdatedNodes, "", "disabled optimization must preserve pre-optimization persistence behavior")
s.Equal(int64(2), rootNode2.serializedNode.GetMetadata().GetLastUpdateVersionedTransition().GetTransitionCount())
enabled = true
nextTransitionCount = 3
_, err = rootNode2.Component(ctx, ComponentRef{})
s.NoError(err)
mutation, err = rootNode2.CloseTransaction()
s.NoError(err)
s.NotContains(mutation.UpdatedNodes, "", "enabled optimization must omit an unchanged node")
s.Equal(int64(2), rootNode2.serializedNode.GetMetadata().GetLastUpdateVersionedTransition().GetTransitionCount())
}