mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? - Visibility writes are added to the Scheduler components. ## How did you test it? - [ ] built - [ ] run locally and tested manually - [x] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s)
332 lines
11 KiB
Go
332 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"
|
|
"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.schedulerSuite.SetupTest()
|
|
s.executor = scheduler.NewInvokerProcessBufferTaskExecutor(scheduler.InvokerTaskExecutorOptions{
|
|
Config: defaultConfig(),
|
|
MetricsHandler: metrics.NoopMetricsHandler,
|
|
BaseLogger: s.logger,
|
|
SpecProcessor: s.specProcessor,
|
|
})
|
|
}
|
|
|
|
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 := s.scheduler.Invoker.Get(ctx)
|
|
|
|
// 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())
|
|
|
|
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)
|
|
}
|
|
}
|