Files
tasks/tests/schedule_test.go
David Reissandrodrigozhou 5e5b9a202c Simplify DescribeSchedule (#4810)
**What changed?**
DescribeSchedule now does just one workflow query. It still sends a
refresh signal if it filters out closed workflows, but it doesn't wait
to observe a state change after the signal.

**Why?**
This extra work (repeated query to observe state change after signal)
was not really useful since we can just filter out the closed workflows
directly, and it made the call more expensive than it needs to be.

**How did you test it?**
existing integration tests
2023-10-06 10:35:12 -05:00

901 lines
30 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package tests
import (
"errors"
"flag"
"fmt"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/pborman/uuid"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
schedulepb "go.temporal.io/api/schedule/v1"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
workflowpb "go.temporal.io/api/workflow/v1"
"go.temporal.io/api/workflowservice/v1"
sdkclient "go.temporal.io/sdk/client"
"go.temporal.io/sdk/converter"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/payload"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/persistence/sql/sqlplugin/mysql"
"go.temporal.io/server/common/persistence/sql/sqlplugin/postgresql"
"go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/searchattribute"
"go.temporal.io/server/service/worker/scheduler"
)
/*
more tests to write:
various validation errors
overlap policies, esp. buffer
worker restart/long-poll activity failure:
get it in a state where it's waiting for a wf to exit, say with bufferone
restart the worker/force activity to fail
terminate the wf
check that new one starts immediately
*/
type (
scheduleIntegrationSuite struct {
*require.Assertions
IntegrationBase
hostPort string
sdkClient sdkclient.Client
worker worker.Worker
taskQueue string
dataConverter converter.DataConverter
}
)
func TestScheduleIntegrationSuite(t *testing.T) {
flag.Parse()
suite.Run(t, new(scheduleIntegrationSuite))
}
func (s *scheduleIntegrationSuite) SetupSuite() {
s.hostPort = "127.0.0.1:7134"
if TestFlags.FrontendAddr != "" {
s.hostPort = TestFlags.FrontendAddr
}
switch TestFlags.PersistenceDriver {
case mysql.PluginNameV8, postgresql.PluginNameV12, sqlite.PluginName:
s.setupSuite("testdata/integration_test_cluster.yaml")
s.Logger.Info(fmt.Sprintf("Running schedule tests with %s/%s persistence", TestFlags.PersistenceType, TestFlags.PersistenceDriver))
default:
s.setupSuite("testdata/integration_test_es_cluster.yaml")
s.Logger.Info("Running schedule tests with Elasticsearch persistence")
}
}
func (s *scheduleIntegrationSuite) TearDownSuite() {
s.tearDownSuite()
}
func (s *scheduleIntegrationSuite) SetupTest() {
s.Assertions = require.New(s.T())
s.dataConverter = newTestDataConverter()
sdkClient, err := sdkclient.Dial(sdkclient.Options{
HostPort: s.hostPort,
Namespace: s.namespace,
DataConverter: s.dataConverter,
})
if err != nil {
s.Logger.Fatal("Error when creating SDK client", tag.Error(err))
}
s.sdkClient = sdkClient
s.taskQueue = s.randomizeStr("tq")
s.worker = worker.New(s.sdkClient, s.taskQueue, worker.Options{})
if err := s.worker.Start(); err != nil {
s.Logger.Fatal("Error when starting worker", tag.Error(err))
}
}
func (s *scheduleIntegrationSuite) TearDownTest() {
s.worker.Stop()
s.sdkClient.Close()
}
func (s *scheduleIntegrationSuite) TestBasics() {
sid := "sched-test-basics"
wid := "sched-test-basics-wf"
wt := "sched-test-basics-wt"
wt2 := "sched-test-basics-wt2"
// switch this to test with search attribute mapper:
// csa := "AliasForCustomKeywordField"
csa := "CustomKeywordField"
wfMemo := payload.EncodeString("workflow memo")
wfSAValue := payload.EncodeString("workflow sa value")
schMemo := payload.EncodeString("schedule memo")
schSAValue := payload.EncodeString("schedule sa value")
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{Interval: timestamp.DurationPtr(5 * time.Second)},
},
Calendar: []*schedulepb.CalendarSpec{
{DayOfMonth: "10", Year: "2010"},
},
CronString: []string{"11 11/11 11 11 1 2011"},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: wid,
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
Memo: &commonpb.Memo{
Fields: map[string]*commonpb.Payload{"wfmemo1": wfMemo},
},
SearchAttributes: &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{csa: wfSAValue},
},
},
},
},
}
req := &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
Memo: &commonpb.Memo{
Fields: map[string]*commonpb.Payload{"schedmemo1": schMemo},
},
SearchAttributes: &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{csa: schSAValue},
},
}
var runs, runs2 int32
workflowFn := func(ctx workflow.Context) error {
workflow.SideEffect(ctx, func(ctx workflow.Context) any {
atomic.AddInt32(&runs, 1)
return 0
})
return nil
}
s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt})
workflow2Fn := func(ctx workflow.Context) error {
workflow.SideEffect(ctx, func(ctx workflow.Context) any {
atomic.AddInt32(&runs2, 1)
return 0
})
return nil
}
s.worker.RegisterWorkflowWithOptions(workflow2Fn, workflow.RegisterOptions{Name: wt2})
// create
createTime := time.Now()
_, err := s.engine.CreateSchedule(NewContext(), req)
s.NoError(err)
// sleep until we see two runs, plus a bit more to ensure that the second run has completed
s.Eventually(func() bool { return atomic.LoadInt32(&runs) == 2 }, 12*time.Second, 500*time.Millisecond)
time.Sleep(1 * time.Second)
// describe
describeResp, err := s.engine.DescribeSchedule(NewContext(), &workflowservice.DescribeScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
})
s.NoError(err)
checkSpec := func(spec *schedulepb.ScheduleSpec) {
s.Equal(schedule.Spec.Interval, spec.Interval)
s.Nil(spec.Calendar)
s.Nil(spec.CronString)
s.ElementsMatch([]*schedulepb.StructuredCalendarSpec{
{
Second: []*schedulepb.Range{{Start: 0, End: 0, Step: 1}},
Minute: []*schedulepb.Range{{Start: 11, End: 11, Step: 1}},
Hour: []*schedulepb.Range{{Start: 11, End: 23, Step: 11}},
DayOfMonth: []*schedulepb.Range{{Start: 11, End: 11, Step: 1}},
Month: []*schedulepb.Range{{Start: 11, End: 11, Step: 1}},
DayOfWeek: []*schedulepb.Range{{Start: 1, End: 1, Step: 1}},
Year: []*schedulepb.Range{{Start: 2011, End: 2011, Step: 1}},
},
{
Second: []*schedulepb.Range{{Start: 0, End: 0, Step: 1}},
Minute: []*schedulepb.Range{{Start: 0, End: 0, Step: 1}},
Hour: []*schedulepb.Range{{Start: 0, End: 0, Step: 1}},
DayOfMonth: []*schedulepb.Range{{Start: 10, End: 10, Step: 1}},
Month: []*schedulepb.Range{{Start: 1, End: 12, Step: 1}},
DayOfWeek: []*schedulepb.Range{{Start: 0, End: 6, Step: 1}},
Year: []*schedulepb.Range{{Start: 2010, End: 2010, Step: 1}},
},
}, spec.StructuredCalendar)
}
checkSpec(describeResp.Schedule.Spec)
s.Equal(enumspb.SCHEDULE_OVERLAP_POLICY_SKIP, describeResp.Schedule.Policies.OverlapPolicy) // set to default value
s.EqualValues(365*24*3600, describeResp.Schedule.Policies.CatchupWindow.Seconds()) // set to default value
s.Equal(schSAValue.Data, describeResp.SearchAttributes.IndexedFields[csa].Data)
s.Nil(describeResp.SearchAttributes.IndexedFields[searchattribute.BinaryChecksums])
s.Nil(describeResp.SearchAttributes.IndexedFields[searchattribute.BuildIds])
s.Equal(schMemo.Data, describeResp.Memo.Fields["schedmemo1"].Data)
s.Equal(wfSAValue.Data, describeResp.Schedule.Action.GetStartWorkflow().SearchAttributes.IndexedFields[csa].Data)
s.Equal(wfMemo.Data, describeResp.Schedule.Action.GetStartWorkflow().Memo.Fields["wfmemo1"].Data)
s.DurationNear(describeResp.Info.CreateTime.Sub(createTime), 0, 3*time.Second)
s.EqualValues(2, describeResp.Info.ActionCount)
s.EqualValues(0, describeResp.Info.MissedCatchupWindow)
s.EqualValues(0, describeResp.Info.OverlapSkipped)
s.EqualValues(0, len(describeResp.Info.RunningWorkflows))
s.EqualValues(2, len(describeResp.Info.RecentActions))
action0 := describeResp.Info.RecentActions[0]
s.WithinRange(*action0.ScheduleTime, createTime, time.Now())
s.True(action0.ScheduleTime.UnixNano()%int64(5*time.Second) == 0)
s.DurationNear(action0.ActualTime.Sub(*action0.ScheduleTime), 0, 3*time.Second)
// list
s.Eventually(func() bool { // wait for visibility
listResp, err := s.engine.ListSchedules(NewContext(), &workflowservice.ListSchedulesRequest{
Namespace: s.namespace,
MaximumPageSize: 5,
})
if err != nil || len(listResp.Schedules) != 1 || listResp.Schedules[0].ScheduleId != sid || len(listResp.Schedules[0].GetInfo().GetRecentActions()) < 2 {
return false
}
s.NoError(err)
entry := listResp.Schedules[0]
s.Equal(sid, entry.ScheduleId)
s.Equal(schSAValue.Data, entry.SearchAttributes.IndexedFields[csa].Data)
s.Equal(schMemo.Data, entry.Memo.Fields["schedmemo1"].Data)
checkSpec(entry.Info.Spec)
s.Equal(wt, entry.Info.WorkflowType.Name)
s.False(entry.Info.Paused)
s.Equal(describeResp.Info.RecentActions, entry.Info.RecentActions) // 2 is below the limit where list entry might be cut off
return true
}, 10*time.Second, 1*time.Second)
// list workflows
wfResp, err := s.engine.ListWorkflowExecutions(NewContext(), &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.namespace,
PageSize: 5,
Query: "",
})
s.NoError(err)
s.GreaterOrEqual(len(wfResp.Executions), 2) // could have had a 3rd run while waiting for visibility
for _, ex := range wfResp.Executions {
s.Equal(wt, ex.Type.Name, "should only see started workflows")
}
ex0 := wfResp.Executions[0]
s.True(strings.HasPrefix(ex0.Execution.WorkflowId, wid))
s.True(ex0.Execution.RunId == describeResp.Info.RecentActions[0].GetStartWorkflowResult().RunId ||
ex0.Execution.RunId == describeResp.Info.RecentActions[1].GetStartWorkflowResult().RunId)
s.Equal(wt, ex0.Type.Name)
s.Nil(ex0.ParentExecution) // not a child workflow
s.Equal(wfMemo.Data, ex0.Memo.Fields["wfmemo1"].Data)
s.Equal(wfSAValue.Data, ex0.SearchAttributes.IndexedFields[csa].Data)
s.Equal(payload.EncodeString(sid).Data, ex0.SearchAttributes.IndexedFields[searchattribute.TemporalScheduledById].Data)
var ex0StartTime time.Time
s.NoError(payload.Decode(ex0.SearchAttributes.IndexedFields[searchattribute.TemporalScheduledStartTime], &ex0StartTime))
s.WithinRange(ex0StartTime, createTime, time.Now())
s.True(ex0StartTime.UnixNano()%int64(5*time.Second) == 0)
// list workflows with namespace division (implementation details here, not public api)
wfResp, err = s.engine.ListWorkflowExecutions(NewContext(), &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.namespace,
PageSize: 5,
Query: fmt.Sprintf("%s = \"%s\"", searchattribute.TemporalNamespaceDivision, scheduler.NamespaceDivision),
})
s.NoError(err)
s.EqualValues(1, len(wfResp.Executions), "should see scheduler workflow")
ex0 = wfResp.Executions[0]
s.Equal(scheduler.WorkflowType, ex0.Type.Name)
// update
schedule.Spec.Interval[0].Phase = timestamp.DurationPtr(1 * time.Second)
schedule.Action.GetStartWorkflow().WorkflowType.Name = wt2
updateTime := time.Now()
_, err = s.engine.UpdateSchedule(NewContext(), &workflowservice.UpdateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
})
s.NoError(err)
// wait for one new run
s.Eventually(func() bool { return atomic.LoadInt32(&runs2) == 1 }, 7*time.Second, 500*time.Millisecond)
// describe again
describeResp, err = s.engine.DescribeSchedule(NewContext(), &workflowservice.DescribeScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
})
s.NoError(err)
s.Equal(schSAValue.Data, describeResp.SearchAttributes.IndexedFields[csa].Data)
s.Equal(schMemo.Data, describeResp.Memo.Fields["schedmemo1"].Data)
s.Equal(wfSAValue.Data, describeResp.Schedule.Action.GetStartWorkflow().SearchAttributes.IndexedFields[csa].Data)
s.Equal(wfMemo.Data, describeResp.Schedule.Action.GetStartWorkflow().Memo.Fields["wfmemo1"].Data)
s.DurationNear(describeResp.Info.UpdateTime.Sub(updateTime), 0, 3*time.Second)
lastAction := describeResp.Info.RecentActions[len(describeResp.Info.RecentActions)-1]
s.True(lastAction.ScheduleTime.UnixNano()%int64(5*time.Second) == 1000000000, lastAction.ScheduleTime.UnixNano())
// pause
_, err = s.engine.PatchSchedule(NewContext(), &workflowservice.PatchScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Patch: &schedulepb.SchedulePatch{
Pause: "because I said so",
},
Identity: "test",
RequestId: uuid.New(),
})
s.NoError(err)
time.Sleep(7 * time.Second)
s.EqualValues(1, atomic.LoadInt32(&runs2), "has not run again")
describeResp, err = s.engine.DescribeSchedule(NewContext(), &workflowservice.DescribeScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
})
s.NoError(err)
s.True(describeResp.Schedule.State.Paused)
s.Equal("because I said so", describeResp.Schedule.State.Notes)
// don't loop to wait for visibility, we already waited 7s from the patch
listResp, err := s.engine.ListSchedules(NewContext(), &workflowservice.ListSchedulesRequest{
Namespace: s.namespace,
MaximumPageSize: 5,
})
s.NoError(err)
s.Equal(1, len(listResp.Schedules))
entry := listResp.Schedules[0]
s.Equal(sid, entry.ScheduleId)
s.True(entry.Info.Paused)
s.Equal("because I said so", entry.Info.Notes)
// finally delete
_, err = s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Identity: "test",
})
s.NoError(err)
describeResp, err = s.engine.DescribeSchedule(NewContext(), &workflowservice.DescribeScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
})
s.Error(err)
s.Eventually(func() bool { // wait for visibility
listResp, err := s.engine.ListSchedules(NewContext(), &workflowservice.ListSchedulesRequest{
Namespace: s.namespace,
MaximumPageSize: 5,
})
s.NoError(err)
return len(listResp.Schedules) == 0
}, 10*time.Second, 1*time.Second)
}
func (s *scheduleIntegrationSuite) TestInput() {
sid := "sched-test-input"
wid := "sched-test-input-wf"
wt := "sched-test-input-wt"
type myData struct {
Stuff string
Things []int
}
input1 := &myData{
Stuff: "here's some data",
Things: []int{7, 8, 9},
}
input2 := map[int]float64{11: 1.4375}
inputPayloads, err := s.dataConverter.ToPayloads(input1, input2)
s.NoError(err)
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{Interval: timestamp.DurationPtr(3 * time.Second)},
},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: wid,
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
Input: inputPayloads,
},
},
},
}
req := &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
}
var runs int32
workflowFn := func(ctx workflow.Context, arg1 *myData, arg2 map[int]float64) error {
workflow.SideEffect(ctx, func(ctx workflow.Context) any {
s.Equal(*input1, *arg1)
s.Equal(input2, arg2)
atomic.AddInt32(&runs, 1)
return 0
})
return nil
}
s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt})
_, err = s.engine.CreateSchedule(NewContext(), req)
s.NoError(err)
s.Eventually(func() bool { return atomic.LoadInt32(&runs) == 1 }, 5*time.Second, 200*time.Millisecond)
// cleanup
_, err = s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Identity: "test",
})
s.NoError(err)
}
func (s *scheduleIntegrationSuite) TestLastCompletionAndError() {
sid := "sched-test-last"
wid := "sched-test-last-wf"
wt := "sched-test-last-wt"
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{Interval: timestamp.DurationPtr(3 * time.Second)},
},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: wid,
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
},
},
},
}
req := &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
}
runs := make(map[string]struct{})
var testComplete int32
workflowFn := func(ctx workflow.Context) (string, error) {
var num int
_ = workflow.SideEffect(ctx, func(ctx workflow.Context) any {
runs[workflow.GetInfo(ctx).WorkflowExecution.ID] = struct{}{}
return len(runs)
}).Get(&num)
var lcr string
if workflow.HasLastCompletionResult(ctx) {
s.NoError(workflow.GetLastCompletionResult(ctx, &lcr))
}
lastErr := workflow.GetLastError(ctx)
switch num {
case 1:
s.Equal("", lcr)
s.NoError(lastErr)
return "this one succeeds", nil
case 2:
s.NoError(lastErr)
s.Equal("this one succeeds", lcr)
return "", errors.New("this one fails")
case 3:
s.Equal("this one succeeds", lcr)
s.ErrorContains(lastErr, "this one fails")
atomic.StoreInt32(&testComplete, 1)
return "done", nil
default:
panic("shouldn't be running anymore")
}
}
s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt})
_, err := s.engine.CreateSchedule(NewContext(), req)
s.NoError(err)
s.Eventually(func() bool { return atomic.LoadInt32(&testComplete) == 1 }, 15*time.Second, 200*time.Millisecond)
// cleanup
_, err = s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Identity: "test",
})
s.NoError(err)
}
func (s *scheduleIntegrationSuite) TestRefresh() {
sid := "sched-test-refresh"
wid := "sched-test-refresh-wf"
wt := "sched-test-refresh-wt"
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{
Interval: timestamp.DurationPtr(30 * time.Second),
// start within three seconds
Phase: timestamp.DurationPtr(time.Duration((time.Now().Unix()+3)%30) * time.Second),
},
},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: wid,
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
WorkflowExecutionTimeout: timestamp.DurationPtr(3 * time.Second),
},
},
},
}
req := &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
}
var runs int32
workflowFn := func(ctx workflow.Context) error {
workflow.SideEffect(ctx, func(ctx workflow.Context) any {
atomic.AddInt32(&runs, 1)
return 0
})
s.NoError(workflow.Sleep(ctx, 10*time.Second)) // longer than execution timeout
return nil
}
s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt})
_, err := s.engine.CreateSchedule(NewContext(), req)
s.NoError(err)
s.Eventually(func() bool { return atomic.LoadInt32(&runs) == 1 }, 6*time.Second, 200*time.Millisecond)
// workflow has started but is now sleeping. it will timeout in 2 seconds.
describeResp, err := s.engine.DescribeSchedule(NewContext(), &workflowservice.DescribeScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
})
s.NoError(err)
s.EqualValues(1, len(describeResp.Info.RunningWorkflows))
events1 := s.getHistory(s.namespace, &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid})
time.Sleep(4 * time.Second)
// now it has timed out, but the scheduler hasn't noticed yet. we can prove it by checking
// its history.
events2 := s.getHistory(s.namespace, &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid})
s.Equal(len(events1), len(events2))
// when we describe we'll force a refresh and see it timed out
describeResp, err = s.engine.DescribeSchedule(NewContext(), &workflowservice.DescribeScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
})
s.NoError(err)
s.EqualValues(0, len(describeResp.Info.RunningWorkflows))
// check scheduler has gotten the refresh and done some stuff. signal is sent without waiting so we need to wait.
s.Eventually(func() bool {
events3 := s.getHistory(s.namespace, &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid})
return len(events3) > len(events2)
}, 5*time.Second, 100*time.Millisecond)
// cleanup
_, err = s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Identity: "test",
})
s.NoError(err)
}
func (s *scheduleIntegrationSuite) TestListBeforeRun() {
sid := "sched-test-list-before-run"
wid := "sched-test-list-before-run-wf"
wt := "sched-test-list-before-run-wt"
// disable per-ns worker so that the schedule workflow never runs
dc := s.testCluster.host.dcClient
dc.OverrideValue(dynamicconfig.WorkerPerNamespaceWorkerCount, 0)
s.testCluster.host.workerService.RefreshPerNSWorkerManager()
time.Sleep(2 * time.Second)
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{Interval: timestamp.DurationPtr(3 * time.Second)},
},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: wid,
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
},
},
},
}
req := &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
}
startTime := time.Now()
_, err := s.engine.CreateSchedule(NewContext(), req)
s.NoError(err)
s.Eventually(func() bool { // wait for visibility
listResp, err := s.engine.ListSchedules(NewContext(), &workflowservice.ListSchedulesRequest{
Namespace: s.namespace,
MaximumPageSize: 5,
})
if err != nil || len(listResp.Schedules) != 1 || listResp.Schedules[0].ScheduleId != sid {
return false
}
s.NoError(err)
entry := listResp.Schedules[0]
s.Equal(sid, entry.ScheduleId)
s.NotNil(entry.Info)
s.Equal(schedule.Spec, entry.Info.Spec)
s.Equal(wt, entry.Info.WorkflowType.Name)
s.False(entry.Info.Paused)
s.Greater(len(entry.Info.FutureActionTimes), 1)
s.True(entry.Info.FutureActionTimes[0].After(startTime))
return true
}, 10*time.Second, 1*time.Second)
// cleanup
_, err = s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Identity: "test",
})
s.NoError(err)
dc.RemoveOverride(dynamicconfig.WorkerPerNamespaceWorkerCount)
s.testCluster.host.workerService.RefreshPerNSWorkerManager()
time.Sleep(2 * time.Second)
}
func (s *scheduleIntegrationSuite) TestRateLimit() {
sid := "sched-test-rate-limit-%d"
wid := "sched-test-rate-limit-wf-%d"
wt := "sched-test-rate-limit-wt"
// Set 1/sec rate limit per namespace. To force this to take effect immediately (instead of
// waiting one minute) we have to cause the whole worker to be stopped and started. The
// sleeps are needed because the refresh is asynchronous, and there's no way to get access
// to the actual rate limiter object to refresh it directly.
s.testCluster.host.dcClient.OverrideValue(dynamicconfig.SchedulerNamespaceStartWorkflowRPS, 1.0)
s.testCluster.host.dcClient.OverrideValue(dynamicconfig.WorkerPerNamespaceWorkerCount, 0)
s.testCluster.host.workerService.RefreshPerNSWorkerManager()
time.Sleep(2 * time.Second)
s.testCluster.host.dcClient.RemoveOverride(dynamicconfig.WorkerPerNamespaceWorkerCount)
s.testCluster.host.workerService.RefreshPerNSWorkerManager()
time.Sleep(2 * time.Second)
var runs int32
workflowFn := func(ctx workflow.Context) error {
workflow.SideEffect(ctx, func(ctx workflow.Context) any {
atomic.AddInt32(&runs, 1)
return 0
})
return nil
}
s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt})
// create 10 copies of the schedule
for i := 0; i < 10; i++ {
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{Interval: timestamp.DurationPtr(1 * time.Second)},
},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: fmt.Sprintf(wid, i),
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
},
},
},
}
_, err := s.engine.CreateSchedule(NewContext(), &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: fmt.Sprintf(sid, i),
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
})
s.NoError(err)
}
time.Sleep(5 * time.Second)
// With no rate limit, we'd see 10/second == 50 workflows run. With a limit of 1/sec, we
// expect to see around 5.
s.Less(atomic.LoadInt32(&runs), int32(10))
// clean up
for i := 0; i < 10; i++ {
_, err := s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: fmt.Sprintf(sid, i),
Identity: "test",
})
s.NoError(err)
}
s.testCluster.host.dcClient.RemoveOverride(dynamicconfig.SchedulerNamespaceStartWorkflowRPS)
s.testCluster.host.dcClient.OverrideValue(dynamicconfig.WorkerPerNamespaceWorkerCount, 0)
s.testCluster.host.workerService.RefreshPerNSWorkerManager()
time.Sleep(2 * time.Second)
s.testCluster.host.dcClient.RemoveOverride(dynamicconfig.WorkerPerNamespaceWorkerCount)
s.testCluster.host.workerService.RefreshPerNSWorkerManager()
}
func (s *scheduleIntegrationSuite) TestNextTimeCache() {
sid := "sched-test-next-time-cache"
wid := "sched-test-next-time-cache-wf"
wt := "sched-test-next-time-cache-wt"
schedule := &schedulepb.Schedule{
Spec: &schedulepb.ScheduleSpec{
Interval: []*schedulepb.IntervalSpec{
{Interval: timestamp.DurationPtr(1 * time.Second)},
},
},
Action: &schedulepb.ScheduleAction{
Action: &schedulepb.ScheduleAction_StartWorkflow{
StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{
WorkflowId: wid,
WorkflowType: &commonpb.WorkflowType{Name: wt},
TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue},
},
},
},
}
req := &workflowservice.CreateScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Schedule: schedule,
Identity: "test",
RequestId: uuid.New(),
}
var runs atomic.Int32
workflowFn := func(ctx workflow.Context) error {
workflow.SideEffect(ctx, func(ctx workflow.Context) any {
runs.Add(1)
return 0
})
return nil
}
s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt})
_, err := s.engine.CreateSchedule(NewContext(), req)
s.NoError(err)
// wait for at least 13 runs
const count = 13
s.Eventually(func() bool { return runs.Load() >= count }, (count+5)*time.Second, 500*time.Millisecond)
// there should be only four side effects for 13 runs, and only two mentioning "Next"
// (cache refills)
events := s.getHistory(s.namespace, &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid})
var sideEffects, nextTimeSideEffects int
for _, e := range events {
if marker := e.GetMarkerRecordedEventAttributes(); marker.GetMarkerName() == "SideEffect" {
sideEffects++
if p, ok := marker.Details["data"]; ok && strings.Contains(payloads.ToString(p), `"Next"`) {
nextTimeSideEffects++
}
}
}
const (
// These match the ones in the scheduler workflow, but they're not exported.
// Change these if those change.
FutureActionCountForList = 5
NextTimeCacheV2Size = 14
// Calculate expected results
expectedCacheSize = NextTimeCacheV2Size - FutureActionCountForList + 1
expectedRefills = (count + expectedCacheSize - 1) / expectedCacheSize
uuidCacheRefills = (count + 9) / 10
)
s.Equal(expectedRefills+uuidCacheRefills, sideEffects)
s.Equal(expectedRefills, nextTimeSideEffects)
// cleanup
_, err = s.engine.DeleteSchedule(NewContext(), &workflowservice.DeleteScheduleRequest{
Namespace: s.namespace,
ScheduleId: sid,
Identity: "test",
})
s.NoError(err)
}