Files
tasks/chasm/lib/scheduler/invoker_process_buffer_task_test.go
Lina JodoinandGitHub 4c140933cf [Scheduled Actions] CHASM Scheduler (#8255)
## What changed?
- Merge PR combining the previous PRs: #8164, #8165, #8166, #8167,
#8168, #8169

## Why?
- All of those PRs are interdependent and cannot be merged individually
cleanly. Sending a single merge commit PR to be merged to main.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
- It's still disabled.
2025-09-02 19:50:45 +00:00

337 lines
11 KiB
Go

package scheduler_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
schedulespb "go.temporal.io/server/api/schedule/v1"
"go.temporal.io/server/chasm"
"go.temporal.io/server/chasm/lib/scheduler"
"go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/util"
"go.temporal.io/server/service/history/tasks"
"google.golang.org/protobuf/types/known/timestamppb"
)
type invokerProcessBufferTaskSuite struct {
schedulerSuite
executor *scheduler.InvokerProcessBufferTaskExecutor
}
func TestInvokerProcessBufferTaskSuite(t *testing.T) {
suite.Run(t, &invokerProcessBufferTaskSuite{})
}
func (s *invokerProcessBufferTaskSuite) SetupTest() {
s.SetupSuite()
s.executor = scheduler.NewInvokerProcessBufferTaskExecutor(scheduler.InvokerTaskExecutorOptions{
Config: defaultConfig(),
MetricsHandler: metrics.NoopMetricsHandler,
BaseLogger: s.logger,
})
}
type processBufferTestCase struct {
InitialBufferedStarts []*schedulespb.BufferedStart
InitialCancelWorkflows []*commonpb.WorkflowExecution
InitialTerminateWorkflows []*commonpb.WorkflowExecution
InitialRunningWorkflows []*commonpb.WorkflowExecution
ExpectedBufferedStarts int
ExpectedRunningWorkflows int
ExpectedTerminateWorkflows int
ExpectedCancelWorkflows int
ExpectedOverlapSkipped int64
ExpectedMissedCatchupWindow int64
ValidateInvoker func(invoker *scheduler.Invoker)
}
// ProcessBuffer attempts all buffered starts with ALLOW_ALL policy.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_AllowAll() {
startTime := timestamppb.New(s.timeSource.Now())
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req1",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
},
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req2",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
},
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req3",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
ExpectedBufferedStarts: 3,
ExpectedOverlapSkipped: 0,
ValidateInvoker: func(invoker *scheduler.Invoker) {
s.Equal(3, len(util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool {
return start.Attempt > 0
})))
},
})
}
// ProcessBuffer processes a start that missed the catchup window.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_MissedCatchupWindow() {
now := s.timeSource.Now()
startTime := now.Add(-defaultCatchupWindow * 2)
startTimestamp := timestamppb.New(startTime)
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTimestamp,
ActualTime: startTimestamp,
DesiredTime: startTimestamp,
Manual: false,
RequestId: "req1",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
ExpectedBufferedStarts: 0,
ExpectedOverlapSkipped: 0,
ExpectedMissedCatchupWindow: 1,
})
}
// ProcessBuffer defers a start (from overlap policy) by placing it into NewBuffer.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BufferOne() {
startTime := timestamppb.New(s.timeSource.Now())
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req1",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE,
},
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req2",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE,
},
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req3",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE,
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
// Because no workflows are running, we'll immediately kick off one
// BufferedStart, and then buffer the next. This leaves us with 1 ready start,
// and 1 still buffered.
ExpectedBufferedStarts: 2,
ExpectedOverlapSkipped: 1,
ValidateInvoker: func(invoker *scheduler.Invoker) {
// Only one start should be set for execution (Attempt > 0)
s.Equal(1, len(util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool {
return start.Attempt > 0
})))
},
})
}
// ProcessBuffer is scheduled with an empty buffer.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_Empty() {
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: nil,
})
}
// ProcessBuffer is scheduled with a buffer of starts all backing off.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BackingOff() {
startTime := timestamppb.New(s.timeSource.Now())
backoffTime := startTime.AsTime().Add(30 * time.Minute)
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req1",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
Attempt: 2,
BackoffTime: timestamppb.New(backoffTime),
},
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: true,
RequestId: "req2",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
Attempt: 3,
BackoffTime: timestamppb.New(backoffTime),
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
ExpectedBufferedStarts: 2,
})
}
// ProcessBuffer is scheduled with a start that was backing off, but ready to retry.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BackingOffReady() {
startTime := timestamppb.New(s.timeSource.Now())
backoffTime := s.timeSource.Now().Add(-1 * time.Minute)
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "req1",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL,
Attempt: 2,
BackoffTime: timestamppb.New(backoffTime),
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
ExpectedBufferedStarts: 1,
ValidateInvoker: func(invoker *scheduler.Invoker) {
// The start should be ready for execution (Attempt > 0)
s.Equal(1, len(util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool {
return start.Attempt > 0
})))
},
})
}
// A buffered start with an overlap policy to terminate other workflows is processed.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsTerminate() {
// Add a running workflow to the Scheduler.
initialRunningWorkflows := []*commonpb.WorkflowExecution{{
WorkflowId: "existing-wf",
RunId: "existing-run",
}}
// Set up the BufferedStart with a policy that will terminate existing workflows.
startTime := timestamppb.New(s.timeSource.Now())
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "new-wf",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER,
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
InitialRunningWorkflows: initialRunningWorkflows,
// Buffer should still contain the buffered start. The existing workflow will still
// remain in RunningWorkflows as well, since it is the Watcher's job to remove it
// after termination/cancelation takes effect.
ExpectedBufferedStarts: 1,
ExpectedRunningWorkflows: 1,
ExpectedTerminateWorkflows: 1,
})
}
// A buffered start with an overlap policy to cancel other workflows is processed.
func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsCancel() {
// Add a running workflow to the Scheduler.
initialRunningWorkflows := []*commonpb.WorkflowExecution{{
WorkflowId: "existing-wf",
RunId: "existing-run",
}}
// Set up the BufferedStart with a policy that will cancel existing workflows.
startTime := timestamppb.New(s.timeSource.Now())
bufferedStarts := []*schedulespb.BufferedStart{
{
NominalTime: startTime,
ActualTime: startTime,
DesiredTime: startTime,
Manual: false,
RequestId: "new-wf",
OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER,
},
}
s.runProcessBufferTestCase(&processBufferTestCase{
InitialBufferedStarts: bufferedStarts,
InitialRunningWorkflows: initialRunningWorkflows,
// Buffer should still contain the buffered start. The existing workflow will still
// remain in RunningWorkflows as well, since it is the Watcher's job to remove it
// after termination/cancelation takes effect.
ExpectedBufferedStarts: 1,
ExpectedRunningWorkflows: 1,
ExpectedCancelWorkflows: 1,
})
}
func (s *invokerProcessBufferTaskSuite) runProcessBufferTestCase(c *processBufferTestCase) {
ctx := s.newMutableContext()
invoker, err := s.scheduler.Invoker.Get(ctx)
s.NoError(err)
// Set up initial state
invoker.BufferedStarts = c.InitialBufferedStarts
invoker.CancelWorkflows = c.InitialCancelWorkflows
invoker.TerminateWorkflows = c.InitialTerminateWorkflows
s.scheduler.Info.RunningWorkflows = c.InitialRunningWorkflows
// Set LastProcessedTime to current time to ensure time checks pass
invoker.LastProcessedTime = timestamppb.New(s.timeSource.Now())
// Clear old tasks and run the process buffer task
s.addedTasks = make([]tasks.Task, 0)
err = s.executor.Execute(ctx, invoker, chasm.TaskAttributes{}, &schedulerpb.InvokerProcessBufferTask{})
s.NoError(err)
_, err = s.node.CloseTransaction()
s.NoError(err)
// Validate the results
s.Equal(c.ExpectedBufferedStarts, len(invoker.GetBufferedStarts()))
s.Equal(c.ExpectedRunningWorkflows, len(s.scheduler.Info.RunningWorkflows))
s.Equal(c.ExpectedTerminateWorkflows, len(invoker.TerminateWorkflows))
s.Equal(c.ExpectedCancelWorkflows, len(invoker.CancelWorkflows))
s.Equal(c.ExpectedOverlapSkipped, s.scheduler.Info.OverlapSkipped)
s.Equal(c.ExpectedMissedCatchupWindow, s.scheduler.Info.MissedCatchupWindow)
// Callbacks
if c.ValidateInvoker != nil {
c.ValidateInvoker(invoker)
}
}