Files
temporal/common/persistence/execution_manager_test.go
Yichao Yang 146d76a673 CHASM: Skip TrimHistoryBranch for non-workflows (#9165)
## What changed?
- Skip TrimHistoryBranch for non-workflows

## Why?
- Chasm execution has no history and has empty branch token. Attempting
TrimHistoryBranch causing an necessary mutable state load. The actual
trim operation will also fail as the branch token is invalid.
- TrimHistoryBranch is only a best effort clean up operation when
Update/ConflictResolveExecution fails due to a Conflict error. So even
without the fix there's no real impact besides noisy error log which
complains that TrimHistoryBranch has failed.

## 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)
2026-01-28 15:29:46 -08:00

239 lines
8.2 KiB
Go

package persistence_test
import (
"context"
"testing"
"time"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
enumsspb "go.temporal.io/server/api/enums/v1"
historyspb "go.temporal.io/server/api/history/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/chasm"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
p "go.temporal.io/server/common/persistence"
mockp "go.temporal.io/server/common/persistence/mock"
"go.temporal.io/server/common/persistence/serialization"
"go.temporal.io/server/common/persistence/versionhistory"
"go.temporal.io/server/common/testing/testlogger"
"go.temporal.io/server/service/history/tasks"
"go.uber.org/mock/gomock"
)
func newTestUpdateRequest(keys []tasks.Key) *p.UpdateWorkflowExecutionRequest {
toDelete := map[tasks.Category][]tasks.Key{
tasks.CategoryTimer: keys,
}
return &p.UpdateWorkflowExecutionRequest{
ShardID: 1,
RangeID: 1,
Mode: p.UpdateWorkflowModeUpdateCurrent,
ArchetypeID: chasm.WorkflowArchetypeID,
UpdateWorkflowMutation: p.WorkflowMutation{
ExecutionInfo: &persistencespb.WorkflowExecutionInfo{NamespaceId: "ns", WorkflowId: "wid", ExecutionStats: &persistencespb.ExecutionStats{}},
ExecutionState: &persistencespb.WorkflowExecutionState{RunId: "rid", State: enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING},
NextEventID: 2,
BestEffortDeleteTasks: toDelete,
},
}
}
func TestExecutionManager_DeletesTasksWhenEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockp.NewMockExecutionStore(ctrl)
store.EXPECT().GetName().AnyTimes().Return("mock-store")
// Expect the main update call
store.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()).Return(nil)
// Expect CompleteHistoryTask for each key
// Use gomock.Any for ctx and request pointer type
expectedKeys := []tasks.Key{
tasks.NewKey(time.Now().UTC(), 123),
tasks.NewKey(time.Now().UTC().Add(time.Second), 456),
}
// Allow any order
store.EXPECT().CompleteHistoryTask(gomock.Any(), gomock.Any()).Times(len(expectedKeys)).DoAndReturn(
func(_ context.Context, req *p.CompleteHistoryTaskRequest) error {
if req.ShardID != 1 || req.TaskCategory != tasks.CategoryTimer {
t.Fatalf("unexpected CompleteHistoryTask request: %#v", req)
}
return nil
},
)
serializer := serialization.NewSerializer()
em := p.NewExecutionManager(
store,
serializer,
nil,
log.NewNoopLogger(),
dynamicconfig.GetIntPropertyFn(1024*1024),
dynamicconfig.GetBoolPropertyFn(true),
)
_, err := em.UpdateWorkflowExecution(context.Background(), newTestUpdateRequest(expectedKeys))
if err != nil {
t.Fatalf("UpdateWorkflowExecution returned error: %v", err)
}
}
func TestExecutionManager_DoesNotDeleteTasksWhenDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockp.NewMockExecutionStore(ctrl)
store.EXPECT().GetName().AnyTimes().Return("mock-store")
store.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()).Return(nil)
// No CompleteHistoryTask expected
serializer := serialization.NewSerializer()
em := p.NewExecutionManager(
store,
serializer,
nil,
log.NewNoopLogger(),
dynamicconfig.GetIntPropertyFn(1024*1024),
dynamicconfig.GetBoolPropertyFn(false),
)
keys := []tasks.Key{tasks.NewKey(time.Now().UTC(), 789)}
_, err := em.UpdateWorkflowExecution(context.Background(), newTestUpdateRequest(keys))
if err != nil {
t.Fatalf("UpdateWorkflowExecution returned error: %v", err)
}
}
func TestExecutionManager_DeleteTasksError_DoesNotFailUpdate(t *testing.T) {
// Test that errors in CompleteHistoryTask don't cause the UpdateWorkflowExecution to fail.
// Task deletion is best-effort and should only be logged.
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockp.NewMockExecutionStore(ctrl)
store.EXPECT().GetName().AnyTimes().Return("mock-store")
// Expect the main update call to succeed
store.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()).Return(nil)
// Expect CompleteHistoryTask to fail
expectedKeys := []tasks.Key{
tasks.NewKey(time.Now().UTC(), 123),
}
store.EXPECT().CompleteHistoryTask(gomock.Any(), gomock.Any()).Times(len(expectedKeys)).Return(
serviceerror.NewInternal("simulated deletion error"),
)
serializer := serialization.NewSerializer()
em := p.NewExecutionManager(
store,
serializer,
nil,
log.NewNoopLogger(),
dynamicconfig.GetIntPropertyFn(1024*1024),
dynamicconfig.GetBoolPropertyFn(true),
)
// UpdateWorkflowExecution should succeed even though CompleteHistoryTask failed
_, err := em.UpdateWorkflowExecution(context.Background(), newTestUpdateRequest(expectedKeys))
if err != nil {
t.Fatalf("UpdateWorkflowExecution should succeed even if task deletion fails, got error: %v", err)
}
}
func TestExecutionManager_TrimHistoryBranchSkipped_NonWorkflow(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockp.NewMockExecutionStore(ctrl)
store.EXPECT().GetName().AnyTimes().Return("mock-store")
store.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()).Return(&p.ConditionFailedError{Msg: "conflict"})
// trimHistoryNode logic skipped for non-workflow archetype and should be no call to GetWorkflowExecution.
store.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()).Times(0)
serializer := serialization.NewSerializer()
em := p.NewExecutionManager(
store,
serializer,
nil,
// TrimHistoryBranch logic is best effort and only emits error log if failed.
// Capture error logs to verify no unexpected errors.
testlogger.NewTestLogger(t, testlogger.FailOnAnyUnexpectedError),
dynamicconfig.GetIntPropertyFn(1024*1024),
dynamicconfig.GetBoolPropertyFn(false),
)
req := newTestUpdateRequest(nil)
req.ArchetypeID = chasm.WorkflowArchetypeID + 1 // Non-workflow archetype
_, err := em.UpdateWorkflowExecution(context.Background(), req)
if err == nil {
t.Fatal("expected UpdateWorkflowExecution to return error")
}
if _, ok := err.(*p.ConditionFailedError); !ok {
t.Fatalf("expected ConditionFailedError, got %T", err)
}
}
func TestExecutionManager_TrimHistoryBranchSkipped_EmptyBranchToken(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockp.NewMockExecutionStore(ctrl)
store.EXPECT().GetName().AnyTimes().Return("mock-store")
store.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()).Return(&p.ConditionFailedError{Msg: "conflict"})
store.EXPECT().GetHistoryBranchUtil().Times(0)
serializer := serialization.NewSerializer()
versionHistories := versionhistory.NewVersionHistories(&historyspb.VersionHistory{}) // Empty branch token
executionInfo := &persistencespb.WorkflowExecutionInfo{
NamespaceId: "ns",
WorkflowId: "wid",
ExecutionStats: &persistencespb.ExecutionStats{},
VersionHistories: versionHistories,
}
executionState := &persistencespb.WorkflowExecutionState{
RunId: "rid",
State: enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING,
Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING,
}
executionInfoBlob, err := serializer.WorkflowExecutionInfoToBlob(executionInfo)
if err != nil {
t.Fatalf("WorkflowExecutionInfoToBlob error: %v", err)
}
executionStateBlob, err := serializer.WorkflowExecutionStateToBlob(executionState)
if err != nil {
t.Fatalf("WorkflowExecutionStateToBlob error: %v", err)
}
store.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()).Return(
&p.InternalGetWorkflowExecutionResponse{
State: &p.InternalWorkflowMutableState{
ExecutionInfo: executionInfoBlob,
ExecutionState: executionStateBlob,
},
},
nil,
)
em := p.NewExecutionManager(
store,
serializer,
nil,
// TrimHistoryBranch logic is best effort and only emits error log if failed.
// Capture error logs to verify no unexpected errors.
testlogger.NewTestLogger(t, testlogger.FailOnAnyUnexpectedError),
dynamicconfig.GetIntPropertyFn(1024*1024),
dynamicconfig.GetBoolPropertyFn(false),
)
_, err = em.UpdateWorkflowExecution(context.Background(), newTestUpdateRequest(nil))
if err == nil {
t.Fatal("expected UpdateWorkflowExecution to return error")
}
if _, ok := err.(*p.ConditionFailedError); !ok {
t.Fatalf("expected ConditionFailedError, got %T", err)
}
}