mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-31 02:51:51 -07:00
CHASM: Best effort pure task deletion (#8531)
## What changed? - CHASM: Best effort pure task deletion ## Why? - Performance improvement, prevent invalid physical pure tasks from firing. ## 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)
This commit is contained in:
@@ -3,6 +3,7 @@ package chasm
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
enumspb "go.temporal.io/api/enums/v1"
|
||||
enumsspb "go.temporal.io/server/api/enums/v1"
|
||||
@@ -28,9 +29,10 @@ type MockNodeBackend struct {
|
||||
HandleGetNexusCompletion func(ctx context.Context, requestID string) (nexusrpc.OperationCompletion, error)
|
||||
|
||||
// Recorded calls (protected by mu).
|
||||
mu sync.Mutex
|
||||
TasksByCategory map[tasks.Category][]tasks.Task
|
||||
UpdateCalls []struct {
|
||||
mu sync.Mutex
|
||||
TasksByCategory map[tasks.Category][]tasks.Task
|
||||
DeletePureTaskCalls []time.Time
|
||||
UpdateCalls []struct {
|
||||
State enumsspb.WorkflowExecutionState
|
||||
Status enumspb.WorkflowExecutionStatus
|
||||
}
|
||||
@@ -90,6 +92,23 @@ func (m *MockNodeBackend) AddTasks(ts ...tasks.Task) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockNodeBackend) DeleteCHASMPureTasks(maxScheduledTime time.Time) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.DeletePureTaskCalls = append(m.DeletePureTaskCalls, maxScheduledTime)
|
||||
}
|
||||
|
||||
func (m *MockNodeBackend) LastDeletePureTaskCall() time.Time {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if len(m.DeletePureTaskCalls) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return m.DeletePureTaskCalls[len(m.DeletePureTaskCalls)-1]
|
||||
}
|
||||
|
||||
func (m *MockNodeBackend) UpdateWorkflowStateStatus(
|
||||
state enumsspb.WorkflowExecutionState,
|
||||
status enumspb.WorkflowExecutionStatus,
|
||||
|
||||
@@ -184,6 +184,7 @@ type (
|
||||
CurrentVersionedTransition() *persistencespb.VersionedTransition
|
||||
GetWorkflowKey() definition.WorkflowKey
|
||||
AddTasks(...tasks.Task)
|
||||
DeleteCHASMPureTasks(maxScheduledTime time.Time)
|
||||
UpdateWorkflowStateStatus(
|
||||
state enumsspb.WorkflowExecutionState,
|
||||
status enumspb.WorkflowExecutionStatus,
|
||||
@@ -1842,13 +1843,21 @@ func (n *Node) closeTransactionGeneratePhysicalPureTask(
|
||||
firstPureTask *persistencespb.ChasmComponentAttributes_Task,
|
||||
firstTaskNode *Node,
|
||||
) error {
|
||||
if firstPureTask == nil || firstPureTask.PhysicalTaskStatus == physicalTaskStatusCreated {
|
||||
if firstPureTask == nil {
|
||||
n.backend.DeleteCHASMPureTasks(tasks.MaximumKey.FireTime)
|
||||
return nil
|
||||
}
|
||||
|
||||
firstPureTaskScheduledTime := firstPureTask.ScheduledTime.AsTime()
|
||||
n.backend.DeleteCHASMPureTasks(firstPureTaskScheduledTime)
|
||||
|
||||
if firstPureTask.PhysicalTaskStatus == physicalTaskStatusCreated {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.backend.AddTasks(&tasks.ChasmTaskPure{
|
||||
WorkflowKey: n.backend.GetWorkflowKey(),
|
||||
VisibilityTimestamp: firstPureTask.ScheduledTime.AsTime(),
|
||||
VisibilityTimestamp: firstPureTaskScheduledTime,
|
||||
Category: tasks.CategoryTimer,
|
||||
})
|
||||
|
||||
|
||||
@@ -1101,6 +1101,7 @@ func (s *nodeSuite) TestApplyMutation_OutOfOrder() {
|
||||
|
||||
func (s *nodeSuite) TestRefreshTasks() {
|
||||
now := s.timeSource.Now()
|
||||
pureTaskScheduledTime := now.Add(time.Second).UTC()
|
||||
persistenceNodes := map[string]*persistencespb.ChasmNode{
|
||||
"": {
|
||||
Metadata: &persistencespb.ChasmNodeMetadata{
|
||||
@@ -1132,7 +1133,7 @@ func (s *nodeSuite) TestRefreshTasks() {
|
||||
PureTasks: []*persistencespb.ChasmComponentAttributes_Task{
|
||||
{
|
||||
Type: "TestLibrary.test_pure_task",
|
||||
ScheduledTime: timestamppb.New(now.Add(time.Second)),
|
||||
ScheduledTime: timestamppb.New(pureTaskScheduledTime),
|
||||
VersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1},
|
||||
VersionedTransitionOffset: 2,
|
||||
PhysicalTaskStatus: physicalTaskStatusCreated,
|
||||
@@ -1176,6 +1177,7 @@ func (s *nodeSuite) TestRefreshTasks() {
|
||||
s.NoError(err)
|
||||
s.Len(mutation.UpdatedNodes, 2) // TaskStatus for the root node is not reset, so no need to persist it.
|
||||
s.Equal(2, s.nodeBackend.NumTasksAdded())
|
||||
s.Equal(pureTaskScheduledTime, s.nodeBackend.LastDeletePureTaskCall())
|
||||
}
|
||||
|
||||
func (s *nodeSuite) TestCarryOverTaskStatus() {
|
||||
@@ -2050,6 +2052,8 @@ func (s *nodeSuite) TestCloseTransaction_InvalidateComponentTasks() {
|
||||
err = root.closeTransactionUpdateComponentTasks(&persistencespb.VersionedTransition{TransitionCount: 2})
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(tasks.MaximumKey.FireTime, s.nodeBackend.LastDeletePureTaskCall())
|
||||
|
||||
componentAttr := root.serializedNode.Metadata.GetComponentAttributes()
|
||||
s.Empty(componentAttr.PureTasks)
|
||||
s.Len(componentAttr.SideEffectTasks, 1)
|
||||
@@ -2163,6 +2167,8 @@ func (s *nodeSuite) TestCloseTransaction_NewComponentTasks() {
|
||||
mutation, err := root.CloseTransaction()
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(s.timeSource.Now().UTC(), s.nodeBackend.LastDeletePureTaskCall())
|
||||
|
||||
rootAttr := mutation.UpdatedNodes[""].GetMetadata().GetComponentAttributes()
|
||||
s.Len(rootAttr.SideEffectTasks, 1) // Only one valid side effect task.
|
||||
newSideEffectTask := rootAttr.SideEffectTasks[0]
|
||||
@@ -2308,7 +2314,7 @@ func (s *nodeSuite) TestCloseTransaction_ApplyMutation_SideEffectTasks() {
|
||||
}
|
||||
|
||||
func (s *nodeSuite) TestCloseTransaction_ApplyMutation_PureTasks() {
|
||||
now := s.timeSource.Now()
|
||||
now := s.timeSource.Now().UTC()
|
||||
persistenceNodes := map[string]*persistencespb.ChasmNode{
|
||||
"": {
|
||||
Metadata: &persistencespb.ChasmNodeMetadata{
|
||||
@@ -2386,6 +2392,8 @@ func (s *nodeSuite) TestCloseTransaction_ApplyMutation_PureTasks() {
|
||||
mutation, err := root.CloseTransaction()
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(now.Add(time.Minute), s.nodeBackend.LastDeletePureTaskCall())
|
||||
|
||||
// Although only root is mutated in ApplyMutation, we generated a pure task for the child node,
|
||||
// and need to persist that as well.
|
||||
s.Len(mutation.UpdatedNodes, 2)
|
||||
@@ -2565,6 +2573,9 @@ func (s *nodeSuite) TestExecuteImmediatePureTask() {
|
||||
s.NoError(err)
|
||||
s.Len(mutations.UpdatedNodes, 2, "root and subcomponent1 should be updated")
|
||||
s.Empty(mutations.DeletedNodes)
|
||||
|
||||
// immedidate pure tasks will be executed inline and no physical chasm pure task will be generated.
|
||||
s.Equal(tasks.MaximumKey.FireTime, s.nodeBackend.LastDeletePureTaskCall())
|
||||
}
|
||||
|
||||
func (s *nodeSuite) TestEachPureTask() {
|
||||
|
||||
@@ -2602,6 +2602,12 @@ that task will be sent to DLQ.`,
|
||||
"Use real chasm tree implementation instead of the noop one",
|
||||
)
|
||||
|
||||
ChasmMaxInMemoryPureTasks = NewGlobalIntSetting(
|
||||
"history.chasmMaxInMemoryPureTasks",
|
||||
32,
|
||||
`ChasmMaxInMemoryPureTasks is the maximum number of physical pure tasks that can be held in memory for best effort task deletion.`,
|
||||
)
|
||||
|
||||
EnableCHASMSchedulerCreation = NewNamespaceBoolSetting(
|
||||
"history.enableCHASMSchedulerCreation",
|
||||
false,
|
||||
|
||||
@@ -64,6 +64,7 @@ type Config struct {
|
||||
MaxCallbacksPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter
|
||||
EnableRequestIdRefLinks dynamicconfig.BoolPropertyFn
|
||||
EnableChasm dynamicconfig.BoolPropertyFn
|
||||
ChasmMaxInMemoryPureTasks dynamicconfig.IntPropertyFn
|
||||
EnableCHASMSchedulerCreation dynamicconfig.BoolPropertyFnWithNamespaceFilter
|
||||
EnableCHASMSchedulerMigration dynamicconfig.BoolPropertyFnWithNamespaceFilter
|
||||
|
||||
@@ -445,6 +446,7 @@ func NewConfig(
|
||||
MaxCallbacksPerWorkflow: dynamicconfig.MaxCallbacksPerWorkflow.Get(dc),
|
||||
EnableRequestIdRefLinks: dynamicconfig.EnableRequestIdRefLinks.Get(dc),
|
||||
EnableChasm: dynamicconfig.EnableChasm.Get(dc),
|
||||
ChasmMaxInMemoryPureTasks: dynamicconfig.ChasmMaxInMemoryPureTasks.Get(dc),
|
||||
|
||||
EnableCHASMSchedulerCreation: dynamicconfig.EnableCHASMSchedulerCreation.Get(dc),
|
||||
EnableCHASMSchedulerMigration: dynamicconfig.EnableCHASMSchedulerMigration.Get(dc),
|
||||
|
||||
@@ -286,6 +286,8 @@ type (
|
||||
|
||||
AddTasks(tasks ...tasks.Task)
|
||||
PopTasks() map[tasks.Category][]tasks.Task
|
||||
DeleteCHASMPureTasks(maxScheduledTime time.Time)
|
||||
|
||||
SetUpdateCondition(int64, int64)
|
||||
GetUpdateCondition() (int64, int64)
|
||||
|
||||
|
||||
@@ -1736,6 +1736,18 @@ func (mr *MockMutableStateMockRecorder) CurrentVersionedTransition() *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentVersionedTransition", reflect.TypeOf((*MockMutableState)(nil).CurrentVersionedTransition))
|
||||
}
|
||||
|
||||
// DeleteCHASMPureTasks mocks base method.
|
||||
func (m *MockMutableState) DeleteCHASMPureTasks(maxScheduledTime time.Time) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "DeleteCHASMPureTasks", maxScheduledTime)
|
||||
}
|
||||
|
||||
// DeleteCHASMPureTasks indicates an expected call of DeleteCHASMPureTasks.
|
||||
func (mr *MockMutableStateMockRecorder) DeleteCHASMPureTasks(maxScheduledTime any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCHASMPureTasks", reflect.TypeOf((*MockMutableState)(nil).DeleteCHASMPureTasks), maxScheduledTime)
|
||||
}
|
||||
|
||||
// DeleteSignalRequested mocks base method.
|
||||
func (m *MockMutableState) DeleteSignalRequested(requestID string) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -227,6 +227,21 @@ type (
|
||||
wftScheduleToStartTimeoutTask *tasks.WorkflowTaskTimeoutTask
|
||||
wftStartToCloseTimeoutTask *tasks.WorkflowTaskTimeoutTask
|
||||
|
||||
// In-memory storage for CHASM pure tasks. These are set when CHASM pure tasks are generated and used to
|
||||
// delete them when then are no longer needed. (i.e. when the task's scheduled time is after that of the
|
||||
// earliest valid CHASM pure task's).
|
||||
//
|
||||
// Those pure tasks are mostly reverse ordered by their scheduled time (the VisibilityTimestamp field).
|
||||
// Since a physical pure task is only generated when there's no other pure task with an earlier scheduled time,
|
||||
// simply appending new pure tasks to the end of the slice maintains the order.
|
||||
//
|
||||
// NOTE: shard context may move those tasks' scheduled time to the future if they are earlier than the timer queue's
|
||||
// max read level (otherwise those tasks won't be loaded), which may potentially break the reverse order.
|
||||
// That is fine, however, as in the worst case we just delete fewer tasks than we could have, but we will never delete
|
||||
// tasks that are still needed (all tasks deleted are those having an earlier scheduled time than what's needed).
|
||||
// Task deletion is just a best-effort optimization after all, so not complicating the logic to account for that here.
|
||||
chasmPureTasks []*tasks.ChasmTaskPure
|
||||
|
||||
// Do not rely on this, this is only updated on
|
||||
// Load() and closeTransactionXXX methods. So when
|
||||
// a transaction is in progress, this value will be
|
||||
@@ -6138,14 +6153,47 @@ func (ms *MutableStateImpl) AddTasks(
|
||||
ms.logger.Info("Dropped long duration scheduled task.", tasks.Tags(task)...)
|
||||
continue
|
||||
}
|
||||
|
||||
if chasmPureTask, ok := task.(*tasks.ChasmTaskPure); ok {
|
||||
ms.chasmPureTasks = append(ms.chasmPureTasks, chasmPureTask)
|
||||
maxPureTasks := ms.config.ChasmMaxInMemoryPureTasks()
|
||||
if len(ms.chasmPureTasks) > maxPureTasks {
|
||||
// Since tasks are reverse ordered by their scheduled time, tasks in the beginning are those
|
||||
// - Generated a long time ago
|
||||
// - Scheduled time is far in the future
|
||||
// both types of tasks are likely to already be persisted in DB and best-effort deletion won't help,
|
||||
// so drop them from the in-memory list first.
|
||||
ms.chasmPureTasks = ms.chasmPureTasks[len(ms.chasmPureTasks)-maxPureTasks:]
|
||||
}
|
||||
}
|
||||
|
||||
ms.InsertTasks[category] = append(ms.InsertTasks[category], task)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MutableStateImpl) PopTasks() map[tasks.Category][]tasks.Task {
|
||||
insterTasks := ms.InsertTasks
|
||||
insertTasks := ms.InsertTasks
|
||||
ms.InsertTasks = make(map[tasks.Category][]tasks.Task)
|
||||
return insterTasks
|
||||
return insertTasks
|
||||
}
|
||||
|
||||
func (ms *MutableStateImpl) DeleteCHASMPureTasks(maxScheduledTime time.Time) {
|
||||
for lastTaskIdx := len(ms.chasmPureTasks) - 1; lastTaskIdx >= 0; lastTaskIdx-- {
|
||||
task := ms.chasmPureTasks[lastTaskIdx]
|
||||
if !task.GetVisibilityTime().Before(maxScheduledTime) {
|
||||
ms.chasmPureTasks = ms.chasmPureTasks[:lastTaskIdx+1]
|
||||
return
|
||||
}
|
||||
|
||||
ms.BestEffortDeleteTasks[tasks.CategoryTimer] = append(
|
||||
ms.BestEffortDeleteTasks[tasks.CategoryTimer],
|
||||
task.GetKey(),
|
||||
)
|
||||
}
|
||||
|
||||
// If we reach here, all tasks have visibility time before maxScheduledTime
|
||||
// and need to be deleted.
|
||||
ms.chasmPureTasks = ms.chasmPureTasks[:0]
|
||||
}
|
||||
|
||||
func (ms *MutableStateImpl) SetUpdateCondition(
|
||||
|
||||
@@ -5358,3 +5358,82 @@ func (s *mutableStateSuite) TestHasRequestID_EmptyExecutionState() {
|
||||
s.False(s.mutableState.HasRequestID(requestID), "Should return false for request ID: %s", requestID)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mutableStateSuite) TestAddTasks_CHASMPureTask() {
|
||||
s.mockConfig.ChasmMaxInMemoryPureTasks = dynamicconfig.GetIntPropertyFn(5)
|
||||
totalTasks := 2 * s.mockConfig.ChasmMaxInMemoryPureTasks()
|
||||
|
||||
visTimestamp := s.mockShard.GetTimeSource().Now()
|
||||
for i := 0; i < totalTasks; i++ {
|
||||
task := &tasks.ChasmTaskPure{
|
||||
VisibilityTimestamp: visTimestamp,
|
||||
Category: tasks.CategoryTimer,
|
||||
}
|
||||
s.mutableState.AddTasks(task)
|
||||
s.LessOrEqual(len(s.mutableState.chasmPureTasks), s.mockConfig.ChasmMaxInMemoryPureTasks())
|
||||
|
||||
visTimestamp = visTimestamp.Add(-time.Minute)
|
||||
}
|
||||
|
||||
s.mockConfig.ChasmMaxInMemoryPureTasks = dynamicconfig.GetIntPropertyFn(2)
|
||||
s.mutableState.AddTasks(&tasks.ChasmTaskPure{
|
||||
VisibilityTimestamp: visTimestamp,
|
||||
Category: tasks.CategoryTimer,
|
||||
})
|
||||
s.Len(s.mutableState.chasmPureTasks, 2)
|
||||
}
|
||||
|
||||
func (s *mutableStateSuite) TestDeleteCHASMPureTasks() {
|
||||
now := s.mockShard.GetTimeSource().Now()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
maxScheduledTime time.Time
|
||||
expectedRemaining int
|
||||
}{
|
||||
{
|
||||
name: "none",
|
||||
maxScheduledTime: now,
|
||||
expectedRemaining: 3,
|
||||
},
|
||||
{
|
||||
name: "paritial",
|
||||
maxScheduledTime: now.Add(2 * time.Minute),
|
||||
expectedRemaining: 2,
|
||||
},
|
||||
{
|
||||
name: "all",
|
||||
maxScheduledTime: now.Add(5 * time.Minute),
|
||||
expectedRemaining: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
s.mutableState.chasmPureTasks = []*tasks.ChasmTaskPure{
|
||||
{
|
||||
VisibilityTimestamp: now.Add(3 * time.Minute),
|
||||
Category: tasks.CategoryTimer,
|
||||
},
|
||||
{
|
||||
VisibilityTimestamp: now.Add(2 * time.Minute),
|
||||
Category: tasks.CategoryTimer,
|
||||
},
|
||||
{
|
||||
VisibilityTimestamp: now.Add(time.Minute),
|
||||
Category: tasks.CategoryTimer,
|
||||
},
|
||||
}
|
||||
s.mutableState.BestEffortDeleteTasks = make(map[tasks.Category][]tasks.Key)
|
||||
|
||||
s.mutableState.DeleteCHASMPureTasks(tc.maxScheduledTime)
|
||||
|
||||
s.Len(s.mutableState.chasmPureTasks, tc.expectedRemaining)
|
||||
for _, task := range s.mutableState.chasmPureTasks {
|
||||
s.False(task.VisibilityTimestamp.Before(tc.maxScheduledTime))
|
||||
}
|
||||
|
||||
s.Len(s.mutableState.BestEffortDeleteTasks[tasks.CategoryTimer], 3-tc.expectedRemaining)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user