mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
5231 lines
177 KiB
Go
5231 lines
177 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 (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
commandpb "go.temporal.io/api/command/v1"
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
historypb "go.temporal.io/api/history/v1"
|
|
protocolpb "go.temporal.io/api/protocol/v1"
|
|
querypb "go.temporal.io/api/query/v1"
|
|
"go.temporal.io/api/serviceerror"
|
|
updatepb "go.temporal.io/api/update/v1"
|
|
"go.temporal.io/api/workflowservice/v1"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
"go.temporal.io/server/api/adminservice/v1"
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/dynamicconfig"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/metrics"
|
|
"go.temporal.io/server/common/metrics/metricstest"
|
|
"go.temporal.io/server/common/payloads"
|
|
"go.temporal.io/server/common/testing/protoutils"
|
|
"go.temporal.io/server/common/testing/testvars"
|
|
"go.temporal.io/server/service/history/consts"
|
|
)
|
|
|
|
func (s *FunctionalSuite) startWorkflow(tv *testvars.TestVars) *testvars.TestVars {
|
|
s.T().Helper()
|
|
request := &workflowservice.StartWorkflowExecutionRequest{
|
|
RequestId: tv.Any().String(),
|
|
Namespace: s.namespace,
|
|
WorkflowId: tv.WorkflowID(),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
}
|
|
|
|
startResp, err := s.client.StartWorkflowExecution(NewContext(), request)
|
|
s.NoError(err)
|
|
|
|
return tv.WithRunID(startResp.GetRunId())
|
|
}
|
|
|
|
type updateResponseErr struct {
|
|
response *workflowservice.UpdateWorkflowExecutionResponse
|
|
err error
|
|
}
|
|
|
|
// TODO: extract sendUpdate* methods to separate package.
|
|
|
|
func (s *FunctionalSuite) sendUpdate(ctx context.Context, tv *testvars.TestVars, updateID string) <-chan updateResponseErr {
|
|
s.T().Helper()
|
|
return s.sendUpdateInternal(ctx, tv, updateID, nil, false)
|
|
}
|
|
|
|
func (s *FunctionalSuite) sendUpdateNoError(tv *testvars.TestVars, updateID string) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
|
|
s.T().Helper()
|
|
return s.sendUpdateNoErrorInternal(tv, updateID, nil)
|
|
}
|
|
|
|
func (s *FunctionalSuite) sendUpdateNoErrorWaitPolicyAccepted(tv *testvars.TestVars, updateID string) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
|
|
s.T().Helper()
|
|
return s.sendUpdateNoErrorInternal(tv, updateID, &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED})
|
|
}
|
|
|
|
func (s *FunctionalSuite) sendUpdateNoErrorInternal(tv *testvars.TestVars, updateID string, waitPolicy *updatepb.WaitPolicy) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
|
|
s.T().Helper()
|
|
retCh := make(chan *workflowservice.UpdateWorkflowExecutionResponse)
|
|
syncCh := make(chan struct{})
|
|
go func() {
|
|
urCh := s.sendUpdateInternal(NewContext(), tv, updateID, waitPolicy, true)
|
|
// Unblock return only after the server admits update.
|
|
syncCh <- struct{}{}
|
|
// Unblocked when an update result is ready.
|
|
retCh <- (<-urCh).response
|
|
}()
|
|
<-syncCh
|
|
return retCh
|
|
}
|
|
|
|
func (s *FunctionalSuite) sendUpdateInternal(
|
|
ctx context.Context,
|
|
tv *testvars.TestVars,
|
|
updateID string,
|
|
waitPolicy *updatepb.WaitPolicy,
|
|
requireNoError bool,
|
|
) <-chan updateResponseErr {
|
|
|
|
s.T().Helper()
|
|
|
|
updateResultCh := make(chan updateResponseErr)
|
|
go func() {
|
|
updateResp, updateErr := s.client.UpdateWorkflowExecution(ctx, &workflowservice.UpdateWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
WorkflowExecution: tv.WorkflowExecution(),
|
|
WaitPolicy: waitPolicy,
|
|
Request: &updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: tv.UpdateID(updateID)},
|
|
Input: &updatepb.Input{
|
|
Name: tv.HandlerName(),
|
|
Args: payloads.EncodeString("args-value-of-" + tv.UpdateID(updateID)),
|
|
},
|
|
},
|
|
})
|
|
// It is important to do assert here (before writing to channel which doesn't have readers yet)
|
|
// to fail fast without trying to process update in wtHandler.
|
|
if requireNoError {
|
|
require.NoError(s.T(), updateErr)
|
|
}
|
|
updateResultCh <- updateResponseErr{response: updateResp, err: updateErr}
|
|
}()
|
|
s.waitUpdateAdmitted(tv, updateID)
|
|
return updateResultCh
|
|
}
|
|
|
|
func (s *FunctionalSuite) waitUpdateAdmitted(tv *testvars.TestVars, updateID string) {
|
|
s.T().Helper()
|
|
s.Eventuallyf(func() bool {
|
|
pollResp, pollErr := s.client.PollWorkflowExecutionUpdate(NewContext(), &workflowservice.PollWorkflowExecutionUpdateRequest{
|
|
Namespace: s.namespace,
|
|
UpdateRef: &updatepb.UpdateRef{
|
|
WorkflowExecution: tv.WorkflowExecution(),
|
|
UpdateId: tv.UpdateID(updateID),
|
|
},
|
|
WaitPolicy: &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED},
|
|
})
|
|
|
|
if pollErr == nil {
|
|
// This is technically "at least Admitted".
|
|
s.GreaterOrEqual(pollResp.Stage, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED)
|
|
return true
|
|
}
|
|
if pollErr.Error() != fmt.Sprintf("update %q not found", tv.UpdateID(updateID)) {
|
|
s.T().Log("received error from Update poll: ", pollErr)
|
|
return true
|
|
}
|
|
|
|
// Poll beat send in race - poll again!
|
|
return false
|
|
}, 5*time.Second, 10*time.Millisecond, "update %s did not reach Admitted stage", updateID)
|
|
}
|
|
|
|
func (s *FunctionalSuite) pollUpdate(tv *testvars.TestVars, updateID string, waitPolicy *updatepb.WaitPolicy) (*workflowservice.PollWorkflowExecutionUpdateResponse, error) {
|
|
s.T().Helper()
|
|
return s.client.PollWorkflowExecutionUpdate(NewContext(), &workflowservice.PollWorkflowExecutionUpdateRequest{
|
|
Namespace: s.namespace,
|
|
UpdateRef: &updatepb.UpdateRef{
|
|
WorkflowExecution: tv.WorkflowExecution(),
|
|
UpdateId: tv.UpdateID(updateID),
|
|
},
|
|
WaitPolicy: waitPolicy,
|
|
})
|
|
}
|
|
|
|
// Simulating a graceful shard closure. The shard finalizer will clear the workflow context,
|
|
// any update requests are aborted and the frontend retries any in-flight update requests.
|
|
func (s *FunctionalSuite) clearUpdateRegistryAndAbortPendingUpdates(tv *testvars.TestVars) {
|
|
s.closeShard(tv.WorkflowID())
|
|
}
|
|
|
|
// Simulating an unexpected loss of the update registry due to a crash. The shard finalizer won't run,
|
|
// therefore the workflow context is NOT cleared, pending update requests are NOT aborted and will time out.
|
|
func (s *FunctionalSuite) loseUpdateRegistryAndAbandonPendingUpdates(tv *testvars.TestVars) {
|
|
s.OverrideDynamicConfig(dynamicconfig.ShardFinalizerTimeout, 0)
|
|
defer s.testCluster.host.dcClient.RemoveOverride(dynamicconfig.ShardFinalizerTimeout)
|
|
s.closeShard(tv.WorkflowID())
|
|
}
|
|
|
|
func (s *FunctionalSuite) speculativeWorkflowTaskOutcomes(
|
|
snap map[string][]*metricstest.CapturedRecording,
|
|
) (commits, rollbacks int) {
|
|
for _ = range snap[metrics.SpeculativeWorkflowTaskCommits.Name()] {
|
|
commits += 1
|
|
}
|
|
for _ = range snap[metrics.SpeculativeWorkflowTaskRollbacks.Name()] {
|
|
rollbacks += 1
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_EmptySpeculativeWorkflowTask_AcceptComplete() {
|
|
testCases := []struct {
|
|
Name string
|
|
UseRunID bool
|
|
}{
|
|
{
|
|
Name: "with RunID",
|
|
UseRunID: true,
|
|
},
|
|
{
|
|
Name: "without RunID",
|
|
UseRunID: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
runID := tv.RunID()
|
|
if !tc.UseRunID {
|
|
// Clear RunID in tv to test code paths when APIs have to fetch current RunID themselves.
|
|
tv = tv.WithRunID("")
|
|
}
|
|
|
|
capture := s.testCluster.host.captureMetricsHandler.StartCapture()
|
|
defer s.testCluster.host.captureMetricsHandler.StopCapture(capture)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT events are not written to the history yet.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res.NewTask)
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
// Test non-blocking poll
|
|
for _, waitPolicy := range []*updatepb.WaitPolicy{{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED}, nil} {
|
|
pollUpdateResp, err := s.pollUpdate(tv, "1", waitPolicy)
|
|
s.NoError(err)
|
|
s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, pollUpdateResp.Stage)
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, pollUpdateResp.Outcome.GetSuccess()))
|
|
// Even if tv doesn't have RunID, it should be returned as part of UpdateRef.
|
|
s.Equal(runID, pollUpdateResp.UpdateRef.GetWorkflowExecution().RunId)
|
|
}
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
commits, rollbacks := s.speculativeWorkflowTaskOutcomes(capture.Snapshot())
|
|
s.Equal(1, commits)
|
|
s.Equal(0, rollbacks)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Was speculative WT...
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted // ...and events were written to the history when WT completes.
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker.
|
|
9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8}
|
|
`, events)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_NotEmptySpeculativeWorkflowTask_AcceptComplete() {
|
|
testCases := []struct {
|
|
Name string
|
|
UseRunID bool
|
|
}{
|
|
{
|
|
Name: "with RunID",
|
|
UseRunID: true,
|
|
},
|
|
{
|
|
Name: "without RunID",
|
|
UseRunID: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
if !tc.UseRunID {
|
|
tv = tv.WithRunID("")
|
|
}
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with update unrelated command.
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID(),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}}, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled // Speculative WFT with ActivityTaskScheduled(5) event after WorkflowTaskCompleted(4).
|
|
7 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(6, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled // Speculative WFT was persisted when completed (event 8)
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 6} // WTScheduled event which delivered update to the worker.
|
|
10 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 9}
|
|
`, events)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_FirstNormalScheduledWorkflowTask_AcceptComplete() {
|
|
|
|
testCases := []struct {
|
|
Name string
|
|
UseRunID bool
|
|
}{
|
|
{
|
|
Name: "with RunID",
|
|
UseRunID: true,
|
|
},
|
|
{
|
|
Name: "without RunID",
|
|
UseRunID: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
if !tc.UseRunID {
|
|
tv = tv.WithRunID("")
|
|
}
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted // First normal WT. No speculative WT was created.
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(2, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(1, wtHandlerCalls)
|
|
s.Equal(1, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker.
|
|
6 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5}
|
|
`, events)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_NormalScheduledWorkflowTask_AcceptComplete() {
|
|
|
|
testCases := []struct {
|
|
Name string
|
|
UseRunID bool
|
|
}{
|
|
{
|
|
Name: "with RunID",
|
|
UseRunID: true,
|
|
},
|
|
{
|
|
Name: "without RunID",
|
|
UseRunID: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
if !tc.UseRunID {
|
|
tv = tv.WithRunID("")
|
|
}
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled // This WT was already created by signal and no speculative WT was created.
|
|
7 WorkflowTaskStarted`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Require().True(len(task.Messages) > 0, "Task has no messages", task)
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(6, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
// Send signal to schedule new WT.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow. It will be attached to existing WT.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 6} // WTScheduled event which delivered update to the worker.
|
|
10 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 9}
|
|
`, events)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_RunningWorkflowTask_NewEmptySpeculativeWorkflowTask_Rejected() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
capture := s.testCluster.host.captureMetricsHandler.StartCapture()
|
|
defer s.testCluster.host.captureMetricsHandler.StopCapture(capture)
|
|
|
|
var updateResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Send update after 1st WT has started.
|
|
updateResultCh = s.sendUpdateNoError(tv, "1")
|
|
// Completes WT with empty command list to create next WFT w/o events.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT2 which was created while completing WT1.
|
|
6 WorkflowTaskStarted`, task.History)
|
|
// Message handler rejects update.
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted // Speculative WT2 disappeared and new normal WT was created.
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Len(task.Messages, 1)
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT which starts 1st update.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
wt1Resp := res.NewTask
|
|
|
|
// Reject update in 2nd WT.
|
|
wt2Resp, err := poller.HandlePartialWorkflowTask(wt1Resp.GetWorkflowTask(), false)
|
|
s.NoError(err)
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(3, wt2Resp.ResetHistoryEventId)
|
|
|
|
// Send signal to create WT.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
commits, rollbacks := s.speculativeWorkflowTaskOutcomes(capture.Snapshot())
|
|
s.Equal(0, commits)
|
|
s.Equal(1, rollbacks)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_RunningWorkflowTask_NewNotEmptySpeculativeWorkflowTask_Rejected() {
|
|
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
var updateResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Send update after 1st WT has started.
|
|
updateResultCh = s.sendUpdateNoError(tv, "1")
|
|
// Completes WT with update unrelated commands to create events that will be in the next speculative WFT.
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID(),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}}, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled // Speculative WFT2 with event (5) which was created while completing WFT1.
|
|
7 WorkflowTaskStarted`, task.History)
|
|
// Message handler rejects update.
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted // Empty speculative WFT was written in to the history because it shipped events.
|
|
9 ActivityTaskStarted
|
|
10 ActivityTaskCompleted
|
|
11 WorkflowTaskScheduled
|
|
12 WorkflowTaskStarted
|
|
`, task.History)
|
|
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Len(task.Messages, 1)
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(6, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) {
|
|
return tv.Any().Payloads(), false, nil
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
ActivityTaskHandler: atHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT which starts 1st update.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
wt1Resp := res.NewTask
|
|
|
|
// Reject update in 2nd WT.
|
|
wt2Resp, err := poller.HandlePartialWorkflowTask(wt1Resp.GetWorkflowTask(), false)
|
|
s.NoError(err)
|
|
s.NotNil(wt2Resp)
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, wt2Resp.ResetHistoryEventId)
|
|
|
|
// Schedule new WFT.
|
|
err = poller.PollAndProcessActivityTask(false)
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
s.EqualValues(0, completeWorkflowResp.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 ActivityTaskStarted
|
|
10 ActivityTaskCompleted
|
|
11 WorkflowTaskScheduled
|
|
12 WorkflowTaskStarted
|
|
13 WorkflowTaskCompleted
|
|
14 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_CompletedWorkflow() {
|
|
s.Run("receive outcome from completed Update", func() {
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
res := s.UpdateAcceptCompleteCommands(tv, "1")
|
|
res = append(res, &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
})
|
|
return res, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
return s.UpdateAcceptCompleteMessages(tv, task.Messages[0], "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
// Send Update request.
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Complete Update and Workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
// Receive Update result.
|
|
updateResult1 := <-updateResultCh
|
|
s.NotNil(updateResult1.GetOutcome().GetSuccess())
|
|
|
|
// Send same Update request again, receiving the same Update result.
|
|
updateResultCh = s.sendUpdateNoError(tv, "1")
|
|
updateResult2 := <-updateResultCh
|
|
s.EqualValues(updateResult1, updateResult2)
|
|
})
|
|
|
|
s.Run("receive error from accepted Update", func() {
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
res := s.UpdateAcceptCommands(tv, "1")
|
|
res = append(res, &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
})
|
|
return res, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
return s.UpdateAcceptMessages(tv, task.Messages[0], "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
// Send Update request.
|
|
updateResultCh := s.sendUpdate(NewContext(), tv, "1")
|
|
|
|
// Accept Update and complete Workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
// Receive Update result.
|
|
updateResult1 := <-updateResultCh
|
|
s.Error(updateResult1.err, consts.ErrWorkflowCompleted)
|
|
|
|
// Send same Update request again, receiving the same error.
|
|
updateResultCh = s.sendUpdate(NewContext(), tv, "1")
|
|
updateResult2 := <-updateResultCh
|
|
s.Error(updateResult2.err, consts.ErrWorkflowCompleted)
|
|
})
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_ValidateWorkerMessages() {
|
|
testCases := []struct {
|
|
Name string
|
|
RespondWorkflowTaskError string
|
|
MessageFn func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message
|
|
CommandFn func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command
|
|
}{
|
|
{
|
|
Name: "message-update-id-not-found-and-accepted-request-not-set",
|
|
RespondWorkflowTaskError: "wasn't found",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-accepted"),
|
|
ProtocolInstanceId: tv.WithUpdateID("bogus-update-id").UpdateID(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: nil, // Important not to pass original request back.
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "message-update-id-not-found-and-accepted-request-is-set",
|
|
RespondWorkflowTaskError: "",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-accepted"),
|
|
ProtocolInstanceId: tv.WithUpdateID("lost-update-id").UpdateID(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest, // Update will be resurrected from original request.
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "command-reference-missed-message",
|
|
RespondWorkflowTaskError: "referenced absent message ID",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.Any().String(),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest,
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "complete-without-accept",
|
|
RespondWorkflowTaskError: "invalid state transition attempted",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-completed"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Response{
|
|
Meta: updRequest.GetMeta(),
|
|
Outcome: &updatepb.Outcome{
|
|
Value: &updatepb.Outcome_Success{
|
|
Success: tv.Any().Payloads(),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-completed"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "accept-twice",
|
|
RespondWorkflowTaskError: "invalid state transition attempted",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-accepted", "1"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest,
|
|
}),
|
|
},
|
|
{
|
|
Id: tv.MessageID("update-accepted", "2"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest,
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted", "1"),
|
|
}},
|
|
},
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted", "2"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "success-case",
|
|
RespondWorkflowTaskError: "",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-accepted"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest,
|
|
}),
|
|
},
|
|
{
|
|
Id: tv.MessageID("update-completed"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Response{
|
|
Meta: updRequest.GetMeta(),
|
|
Outcome: &updatepb.Outcome{
|
|
Value: &updatepb.Outcome_Success{
|
|
Success: tv.Any().Payloads(),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted"),
|
|
}},
|
|
},
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-completed"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "success-case-no-commands", // PROTOCOL_MESSAGE commands are optional.
|
|
RespondWorkflowTaskError: "",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.Any().String(),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest,
|
|
}),
|
|
},
|
|
{
|
|
Id: tv.Any().String(),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Response{
|
|
Meta: updRequest.GetMeta(),
|
|
Outcome: &updatepb.Outcome{
|
|
Value: &updatepb.Outcome_Success{
|
|
Success: tv.Any().Payloads(),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "invalid-command-order",
|
|
RespondWorkflowTaskError: "invalid state transition attempted",
|
|
MessageFn: func(tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message {
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), reqMsg.GetBody())
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-accepted"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: reqMsg.GetEventId(),
|
|
AcceptedRequest: updRequest,
|
|
}),
|
|
},
|
|
{
|
|
Id: tv.MessageID("update-completed"),
|
|
ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Response{
|
|
Meta: updRequest.GetMeta(),
|
|
Outcome: &updatepb.Outcome{
|
|
Value: &updatepb.Outcome_Success{
|
|
Success: tv.Any().Payloads(),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
},
|
|
CommandFn: func(tv *testvars.TestVars, history *historypb.History) []*commandpb.Command {
|
|
return []*commandpb.Command{
|
|
// Complete command goes before Accept command.
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-completed"),
|
|
}},
|
|
},
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE,
|
|
Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{
|
|
MessageId: tv.MessageID("update-accepted"),
|
|
}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
if tc.CommandFn == nil {
|
|
return nil, nil
|
|
}
|
|
return tc.CommandFn(tv, task.History), nil
|
|
}
|
|
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
if tc.MessageFn == nil {
|
|
return nil, nil
|
|
}
|
|
updRequestMsg := task.Messages[0]
|
|
return tc.MessageFn(tv, updRequestMsg), nil
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
halfSecondTimeoutCtx, cancel := context.WithTimeout(NewContext(), 500*time.Millisecond)
|
|
defer cancel()
|
|
updateResultCh := s.sendUpdate(halfSecondTimeoutCtx, tv, "1")
|
|
|
|
// Process update in workflow.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
updateResult := <-updateResultCh
|
|
if tc.RespondWorkflowTaskError != "" {
|
|
require.Error(s.T(), err, "RespondWorkflowTaskCompleted should return an error contains `%v`", tc.RespondWorkflowTaskError)
|
|
require.Contains(s.T(), err.Error(), tc.RespondWorkflowTaskError)
|
|
|
|
// When worker returns validation error, API caller got timeout error.
|
|
require.Error(s.T(), updateResult.err)
|
|
require.True(s.T(), common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error())
|
|
require.Nil(s.T(), updateResult.response)
|
|
} else {
|
|
require.NoError(s.T(), err)
|
|
require.NoError(s.T(), updateResult.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StickySpeculativeWorkflowTask_AcceptComplete() {
|
|
testCases := []struct {
|
|
Name string
|
|
UseRunID bool
|
|
}{
|
|
{
|
|
Name: "with RunID",
|
|
UseRunID: true,
|
|
},
|
|
{
|
|
Name: "without RunID",
|
|
UseRunID: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
if !tc.UseRunID {
|
|
tv = tv.WithRunID("")
|
|
}
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
// This WT contains partial history because sticky was enabled.
|
|
s.EqualHistory(`
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
StickyTaskQueue: tv.StickyTaskQueue(),
|
|
StickyScheduleToStartTimeout: 3 * time.Second,
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain existing first WT from regular task queue, but respond with sticky queue enabled response, next WT will go to sticky queue.
|
|
_, err := poller.PollAndProcessWorkflowTask(WithRespondSticky)
|
|
s.NoError(err)
|
|
|
|
go func() {
|
|
// Process update in workflow task (it is sticky).
|
|
res, err := poller.PollAndProcessWorkflowTask(WithPollSticky, WithoutRetries)
|
|
require.NoError(s.T(), err)
|
|
require.NotNil(s.T(), res)
|
|
require.EqualValues(s.T(), 0, res.NewTask.ResetHistoryEventId)
|
|
}()
|
|
|
|
// This is to make sure that sticky poller above reached server first.
|
|
// And when update comes, stick poller is already available.
|
|
time.Sleep(500 * time.Millisecond)
|
|
updateResult := <-s.sendUpdateNoError(tv, "1")
|
|
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker.
|
|
9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8}
|
|
`, events)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StickySpeculativeWorkflowTask_AcceptComplete_StickyWorkerUnavailable() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
// Worker gets full history because update was issued after sticky worker is gone.
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
StickyTaskQueue: tv.StickyTaskQueue(),
|
|
StickyScheduleToStartTimeout: 3 * time.Second,
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain existing WT from regular task queue, but respond with sticky enabled response to enable stick task queue.
|
|
_, err := poller.PollAndProcessWorkflowTask(WithRespondSticky, WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
s.Logger.Info("Sleep 10+ seconds to make sure stickyPollerUnavailableWindow time has passed.")
|
|
time.Sleep(10*time.Second + 100*time.Millisecond)
|
|
s.Logger.Info("Sleep 10+ seconds is done.")
|
|
|
|
// Now send an update. It should try sticky task queue first, but got "StickyWorkerUnavailable" error
|
|
// and resend it to normal.
|
|
// This can be observed in wtHandler: if history is partial => sticky task queue is used.
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow task from non-sticky task queue.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker.
|
|
9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8}
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_FirstNormalScheduledWorkflowTask_Reject() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted`, task.History)
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(2, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, updateResp.ResetHistoryEventId)
|
|
|
|
s.Equal(1, wtHandlerCalls)
|
|
s.Equal(1, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled // First normal WT was scheduled before update and therefore all 3 events have to be written even if update was rejected.
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted // Empty completed WT. No new events were created after it.
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_EmptySpeculativeWorkflowTask_Reject() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled // Speculative WT was dropped and history starts from 5 again.
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(3, updateResp.ResetHistoryEventId)
|
|
|
|
// Send signal to create WT.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Process signal and complete workflow.
|
|
res, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled // Speculative WT is not present in the history.
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 WorkflowExecutionCompleted
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_NotEmptySpeculativeWorkflowTask_Reject() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID(),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}}, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled // Speculative WFT will be written to the history because there is ActivityTaskScheduled(5) event.
|
|
7 WorkflowTaskStarted
|
|
`, task.History)
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted // Empty speculative WFT was written to the history because it shipped events.
|
|
9 ActivityTaskStarted
|
|
10 ActivityTaskCompleted
|
|
11 WorkflowTaskScheduled
|
|
12 WorkflowTaskStarted
|
|
`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(6, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) {
|
|
return tv.Any().Payloads(), false, nil
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
ActivityTaskHandler: atHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with normal workflow task")
|
|
|
|
err = poller.PollAndProcessActivityTask(false)
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
res, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled // Speculative WFT (6-8) presents in the history even though update was rejected.
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 ActivityTaskStarted
|
|
10 ActivityTaskCompleted
|
|
11 WorkflowTaskScheduled
|
|
12 WorkflowTaskStarted
|
|
13 WorkflowTaskCompleted
|
|
14 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_1stAccept_2ndAccept_2ndComplete_1stComplete() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted`, task.History)
|
|
return append(s.UpdateAcceptCommands(tv, "1"), &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID("1"),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}), nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted // 1st update is accepted.
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled // New normal WT is created because of the 2nd update.
|
|
8 WorkflowTaskStarted`, task.History)
|
|
return append(s.UpdateAcceptCommands(tv, "2"), &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID("2"),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}), nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted
|
|
10 WorkflowExecutionUpdateAccepted // 2nd update is accepted.
|
|
11 ActivityTaskScheduled
|
|
12 ActivityTaskStarted
|
|
13 ActivityTaskCompleted
|
|
14 WorkflowTaskScheduled
|
|
15 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateCompleteCommands(tv, "2"), nil
|
|
case 4:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted
|
|
10 WorkflowExecutionUpdateAccepted
|
|
11 ActivityTaskScheduled
|
|
12 ActivityTaskStarted
|
|
13 ActivityTaskCompleted
|
|
14 WorkflowTaskScheduled
|
|
15 WorkflowTaskStarted
|
|
16 WorkflowTaskCompleted
|
|
17 WorkflowExecutionUpdateCompleted // 2nd update is completed.
|
|
18 ActivityTaskStarted
|
|
19 ActivityTaskCompleted
|
|
20 WorkflowTaskScheduled
|
|
21 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
var upd1RequestMsg, upd2RequestMsg *protocolpb.Message
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
upd1RequestMsg = task.Messages[0]
|
|
upd1Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd1RequestMsg.GetBody())
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, upd1Request.GetInput().GetArgs()))
|
|
s.EqualValues(2, upd1RequestMsg.GetEventId())
|
|
return s.UpdateAcceptMessages(tv, upd1RequestMsg, "1"), nil
|
|
case 2:
|
|
upd2RequestMsg = task.Messages[0]
|
|
upd2Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd2RequestMsg.GetBody())
|
|
s.Equal("args-value-of-"+tv.UpdateID("2"), decodeString(s, upd2Request.GetInput().GetArgs()))
|
|
s.EqualValues(7, upd2RequestMsg.GetEventId())
|
|
return s.UpdateAcceptMessages(tv, upd2RequestMsg, "2"), nil
|
|
case 3:
|
|
s.NotNil(upd2RequestMsg)
|
|
return s.UpdateCompleteMessages(tv, upd2RequestMsg, "2"), nil
|
|
case 4:
|
|
s.NotNil(upd1RequestMsg)
|
|
return s.UpdateCompleteMessages(tv, upd1RequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) {
|
|
return tv.Any().Payloads(), false, nil
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
ActivityTaskHandler: atHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
updateResultCh1 := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Accept update1 in normal WT1.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
// Send 2nd update and create speculative WT2.
|
|
updateResultCh2 := s.sendUpdateNoError(tv, "2")
|
|
|
|
// Poll for WT2 which 2nd update. Accept update2.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
err = poller.PollAndProcessActivityTask(false)
|
|
s.NoError(err)
|
|
|
|
// Complete update2 in WT3.
|
|
res, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult2 := <-updateResultCh2
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("2"), decodeString(s, updateResult2.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
err = poller.PollAndProcessActivityTask(false)
|
|
s.NoError(err)
|
|
|
|
// Complete update1 in WT4.
|
|
res, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult1 := <-updateResultCh1
|
|
s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult1.Stage)
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult1.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(4, wtHandlerCalls)
|
|
s.Equal(4, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker.
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted
|
|
10 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 7} // WTScheduled event which delivered update to the worker.
|
|
11 ActivityTaskScheduled
|
|
12 ActivityTaskStarted
|
|
13 ActivityTaskCompleted
|
|
14 WorkflowTaskScheduled
|
|
15 WorkflowTaskStarted
|
|
16 WorkflowTaskCompleted
|
|
17 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 10} // 2nd update is completed.
|
|
18 ActivityTaskStarted
|
|
19 ActivityTaskCompleted
|
|
20 WorkflowTaskScheduled
|
|
21 WorkflowTaskStarted
|
|
22 WorkflowTaskCompleted
|
|
23 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} // 1st update is completed.
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_1stAccept_2ndReject_1stComplete() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted`, task.History)
|
|
return append(s.UpdateAcceptCommands(tv, "1"), &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID("1"),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}), nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted // 1st update is accepted.
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled // Speculative WFT with WorkflowExecutionUpdateAccepted(5) event.
|
|
8 WorkflowTaskStarted
|
|
`, task.History)
|
|
// Message handler rejects 2nd update.
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted // Speculative WFT is written to the history because it shipped event.
|
|
10 ActivityTaskStarted
|
|
11 ActivityTaskCompleted
|
|
12 WorkflowTaskScheduled
|
|
13 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
var upd1RequestMsg *protocolpb.Message
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
upd1RequestMsg = task.Messages[0]
|
|
upd1Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd1RequestMsg.GetBody())
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, upd1Request.GetInput().GetArgs()))
|
|
s.EqualValues(2, upd1RequestMsg.GetEventId())
|
|
return s.UpdateAcceptMessages(tv, upd1RequestMsg, "1"), nil
|
|
case 2:
|
|
upd2RequestMsg := task.Messages[0]
|
|
upd2Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd2RequestMsg.GetBody())
|
|
s.Equal("args-value-of-"+tv.UpdateID("2"), decodeString(s, upd2Request.GetInput().GetArgs()))
|
|
s.EqualValues(7, upd2RequestMsg.GetEventId())
|
|
return s.UpdateRejectMessages(tv, upd2RequestMsg, "2"), nil
|
|
case 3:
|
|
s.NotNil(upd1RequestMsg)
|
|
return s.UpdateCompleteMessages(tv, upd1RequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) {
|
|
return tv.Any().Payloads(), false, nil
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
ActivityTaskHandler: atHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
updateResultCh1 := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Accept update1 in WT1.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
// Send 2nd update and create speculative WT2.
|
|
updateResultCh2 := s.sendUpdateNoError(tv, "2")
|
|
|
|
// Poll for WT2 which 2nd update. Reject update2.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with workflow task which had events")
|
|
|
|
updateResult2 := <-updateResultCh2
|
|
s.Equal("rejection-of-"+tv.UpdateID("2"), updateResult2.GetOutcome().GetFailure().GetMessage())
|
|
|
|
err = poller.PollAndProcessActivityTask(false)
|
|
s.NoError(err)
|
|
|
|
// Complete update1 in WT3.
|
|
res, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult1 := <-updateResultCh1
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult1.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker.
|
|
6 ActivityTaskScheduled
|
|
7 WorkflowTaskScheduled
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted // WT which had rejected update.
|
|
10 ActivityTaskStarted
|
|
11 ActivityTaskCompleted
|
|
12 WorkflowTaskScheduled
|
|
13 WorkflowTaskStarted
|
|
14 WorkflowTaskCompleted
|
|
15 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5}
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_Fail() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCommands(tv, "1"), nil
|
|
case 3:
|
|
s.Fail("should not be called because messageHandler returns error")
|
|
return nil, nil
|
|
case 4:
|
|
s.Fail("should not be called because messageHandler returns error")
|
|
return nil, nil
|
|
case 5:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskFailed
|
|
8 WorkflowTaskScheduled
|
|
9 WorkflowTaskStarted`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
// Emulate bug in worker/SDK update handler code. Return malformed acceptance response.
|
|
return []*protocolpb.Message{
|
|
{
|
|
Id: tv.MessageID("update-accepted", "1"),
|
|
ProtocolInstanceId: tv.Any().String(),
|
|
SequencingId: nil,
|
|
Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: updRequestMsg.GetId(),
|
|
AcceptedRequestSequencingEventId: updRequestMsg.GetEventId(),
|
|
AcceptedRequest: nil, // must not be nil.
|
|
}),
|
|
},
|
|
}, nil
|
|
case 3:
|
|
// 2nd attempt has same updates attached to it.
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(8, updRequestMsg.GetEventId())
|
|
wtHandlerCalls++ // because it won't be called for case 3 but counter should be in sync.
|
|
// Fail WT one more time. Although 2nd attempt is normal WT, it is also transient and shouldn't appear in the history.
|
|
// Returning error will cause the poller to fail WT.
|
|
return nil, errors.New("malformed request")
|
|
case 4:
|
|
// 3rd attempt UpdateWorkflowExecution call has timed out but the
|
|
// update is still running
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(8, updRequestMsg.GetEventId())
|
|
wtHandlerCalls++ // because it won't be called for case 4 but counter should be in sync.
|
|
// Fail WT one more time. This is transient WT and shouldn't appear in the history.
|
|
// Returning error will cause the poller to fail WT.
|
|
return nil, errors.New("malformed request")
|
|
case 5:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(NewContext(), 2*time.Second)
|
|
defer cancel()
|
|
updateResultCh := s.sendUpdate(timeoutCtx, tv, "1")
|
|
|
|
// Try to accept update in workflow: get malformed response.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "wasn't found")
|
|
// New normal (but transient) WT will be created but not returned.
|
|
|
|
s.waitUpdateAdmitted(tv, "1")
|
|
// Try to accept update in workflow 2nd time: get error. Poller will fail WT.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
// The error is from RespondWorkflowTaskFailed, which should go w/o error.
|
|
s.NoError(err)
|
|
|
|
// Wait for UpdateWorkflowExecution to timeout.
|
|
// This does NOT remove update from registry
|
|
updateResult := <-updateResultCh
|
|
s.Error(updateResult.err)
|
|
s.True(common.IsContextDeadlineExceededErr(updateResult.err), "UpdateWorkflowExecution must timeout after 2 seconds")
|
|
s.Nil(updateResult.response)
|
|
|
|
// Try to accept update in workflow 3rd time: get error. Poller will fail WT.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
// The error is from RespondWorkflowTaskFailed, which should go w/o error.
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
s.Equal(5, wtHandlerCalls)
|
|
s.Equal(5, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskFailed
|
|
8 WorkflowTaskScheduled
|
|
9 WorkflowTaskStarted
|
|
10 WorkflowTaskCompleted
|
|
11 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StartedSpeculativeWorkflowTask_ConvertToNormalBecauseOfBufferedSignal() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT. Events 5 and 6 are written into the history when signal is received.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
// Send signal which will be buffered. This will persist MS and speculative WT must be converted to normal.
|
|
err := s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionSignaled // It was buffered and got to the history after WT is completed.
|
|
9 WorkflowTaskScheduled
|
|
10 WorkflowTaskStarted`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
// Update is rejected but corresponding speculative WT will be in the history anyway, because it was converted to normal due to buffered signal.
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, updateResp.ResetHistoryEventId)
|
|
|
|
// Complete workflow.
|
|
completeWorkflowResp, err := poller.HandlePartialWorkflowTask(updateResp.GetWorkflowTask(), false)
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
s.Nil(completeWorkflowResp.GetWorkflowTask())
|
|
s.EqualValues(0, completeWorkflowResp.ResetHistoryEventId)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted // Update was rejected on speculative WT, but events 5-7 are in the history because of buffered signal.
|
|
8 WorkflowExecutionSignaled
|
|
9 WorkflowTaskScheduled
|
|
10 WorkflowTaskStarted
|
|
11 WorkflowTaskCompleted
|
|
12 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_ScheduledSpeculativeWorkflowTask_ConvertToNormalBecauseOfSignal() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // It was initially speculative WT but was already converted to normal when signal was received.
|
|
6 WorkflowExecutionSignaled
|
|
7 WorkflowTaskStarted`, task.History)
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
|
|
s.EqualValues(6, updRequestMsg.GetEventId())
|
|
|
|
// Update is rejected but corresponding speculative WT was already converted to normal,
|
|
// and will be in the history anyway.
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Send signal which will NOT be buffered because speculative WT is not started yet (only scheduled).
|
|
// This will persist MS and speculative WT must be converted to normal.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowExecutionSignaled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted // Update was rejected but WT events 5,7,8 are in the history because of signal.
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_StartToCloseTimeout() {
|
|
tv := testvars.New(s.T())
|
|
|
|
capture := s.testCluster.host.captureMetricsHandler.StartCapture()
|
|
defer s.testCluster.host.captureMetricsHandler.StopCapture(capture)
|
|
|
|
request := &workflowservice.StartWorkflowExecutionRequest{
|
|
RequestId: tv.Any().String(),
|
|
Namespace: s.namespace,
|
|
WorkflowId: tv.WorkflowID(),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskTimeout: durationpb.New(1 * time.Second), // Important!
|
|
}
|
|
|
|
startResp, err := s.client.StartWorkflowExecution(NewContext(), request)
|
|
s.NoError(err)
|
|
|
|
tv = tv.WithRunID(startResp.GetRunId())
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
// Emulate slow worker: sleep little more than WT timeout.
|
|
time.Sleep(request.WorkflowTaskTimeout.AsDuration() + 100*time.Millisecond)
|
|
// This doesn't matter because WT times out before update is applied.
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
case 3:
|
|
// Speculative WT timed out and retried as normal WT.
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskTimedOut
|
|
8 WorkflowTaskScheduled {"Attempt":2 } // Transient WT.
|
|
9 WorkflowTaskStarted`, task.History)
|
|
commands := append(s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
&commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
})
|
|
return commands, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Len(task.Messages, 1)
|
|
updRequestMsg := task.Messages[0]
|
|
|
|
// This doesn't matter because WT times out before update is applied.
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
// Update is still in registry and was sent again.
|
|
updRequestMsg := task.Messages[0]
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Try to process update in workflow, but it takes more than WT timeout. So, WT times out.
|
|
_, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.Error(err)
|
|
s.Equal("Workflow task not found.", err.Error())
|
|
|
|
// ensure correct metrics were recorded
|
|
snap := capture.Snapshot()
|
|
|
|
var speculativeWorkflowTaskTimeoutTasks int
|
|
for _, m := range snap[metrics.TaskRequests.Name()] {
|
|
if m.Tags[metrics.OperationTagName] == metrics.TaskTypeTimerActiveTaskSpeculativeWorkflowTaskTimeout {
|
|
speculativeWorkflowTaskTimeoutTasks += 1
|
|
}
|
|
}
|
|
s.Equal(1, speculativeWorkflowTaskTimeoutTasks, "expected 1 speculative workflow task timeout task to be created")
|
|
|
|
var speculativeStartToCloseTimeouts int
|
|
for _, m := range snap[metrics.StartToCloseTimeoutCounter.Name()] {
|
|
if m.Tags[metrics.OperationTagName] == metrics.TaskTypeTimerActiveTaskSpeculativeWorkflowTaskTimeout {
|
|
speculativeStartToCloseTimeouts += 1
|
|
}
|
|
}
|
|
s.Equal(1, speculativeStartToCloseTimeouts, "expected 1 timeout of a speculative workflow task timeout task")
|
|
|
|
// New normal WT was created on server after speculative WT has timed out.
|
|
// It will accept and complete update first and workflow itself with the same WT.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, updateResp.ResetHistoryEventId)
|
|
s.Nil(updateResp.GetWorkflowTask())
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskTimedOut // Timeout of speculative WT writes events 5-7
|
|
8 WorkflowTaskScheduled {"Attempt":2 }
|
|
9 WorkflowTaskStarted
|
|
10 WorkflowTaskCompleted
|
|
11 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 8} // WTScheduled event which delivered update to the worker.
|
|
12 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 11}
|
|
13 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_ScheduleToStartTimeout() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
// Speculative WT timed out on sticky task queue. Server sent full history with sticky timeout event.
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskTimedOut
|
|
7 WorkflowTaskScheduled {"Attempt":1} // Normal WT.
|
|
8 WorkflowTaskStarted`, task.History)
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
// Reject update, but WT still will be in the history due to timeout on sticky queue.
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
StickyTaskQueue: tv.StickyTaskQueue(),
|
|
StickyScheduleToStartTimeout: 1 * time.Second, // Important!
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT and respond with sticky enabled response to enable sticky task queue.
|
|
_, err := poller.PollAndProcessWorkflowTask(WithRespondSticky, WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
s.sendUpdateNoError(tv, "1")
|
|
|
|
s.Logger.Info("Wait for sticky timeout to fire. Sleep poller.StickyScheduleToStartTimeout+ seconds.", tag.NewDurationTag("StickyScheduleToStartTimeout", poller.StickyScheduleToStartTimeout))
|
|
time.Sleep(poller.StickyScheduleToStartTimeout + 100*time.Millisecond)
|
|
s.Logger.Info("Sleep is done.")
|
|
|
|
// Try to process update in workflow, poll from normal task queue.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
s.NotNil(updateResp)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT was written into the history because of timeout.
|
|
6 WorkflowTaskTimedOut
|
|
7 WorkflowTaskScheduled {"Attempt":1} // Second attempt WT is normal WT (clear stickiness reset attempts count).
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted // Normal WT is completed and events are in the history even update was rejected.
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_ScheduleToStartTimeoutOnNormalTaskQueue() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled {"TaskQueue": {"Kind": 1}} // Speculative WT timed out on normal(1) task queue.
|
|
6 WorkflowTaskTimedOut
|
|
7 WorkflowTaskScheduled {"Attempt":1} // Normal WT is scheduled.
|
|
8 WorkflowTaskStarted
|
|
`, task.History)
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(7, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain existing WT from normal task queue.
|
|
_, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
|
|
// Now send an update. It will create a speculative WT on normal task queue,
|
|
// which will time out in 5 seconds and create new normal WT.
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// TODO: it would be nice to shutdown matching before sending an update to emulate case which is actually being tested here.
|
|
// But test infrastructure doesn't support it. 5 seconds sleep will cause same observable effect.
|
|
s.Logger.Info("Sleep 5+ seconds to make sure tasks.SpeculativeWorkflowTaskScheduleToStartTimeout time has passed.")
|
|
time.Sleep(5*time.Second + 100*time.Millisecond)
|
|
s.Logger.Info("Sleep 5+ seconds is done.")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with normal workflow task")
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled {"TaskQueue": {"Kind": 1}} // Speculative WT timed out on normal(1) task queue.
|
|
6 WorkflowTaskTimedOut
|
|
7 WorkflowTaskScheduled {"Attempt":1} // Normal WT is scheduled. Even update was rejected, WT is in the history.
|
|
8 WorkflowTaskStarted
|
|
9 WorkflowTaskCompleted
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StartedSpeculativeWorkflowTask_TerminateWorkflow() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
// Terminate workflow while speculative WT is running.
|
|
_, err := s.client.TerminateWorkflowExecution(NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
WorkflowExecution: tv.WorkflowExecution(),
|
|
Reason: tv.Any().String(),
|
|
})
|
|
s.NoError(err)
|
|
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
oneSecondTimeoutCtx, cancel := context.WithTimeout(NewContext(), 1*time.Second)
|
|
defer cancel()
|
|
updateResultCh := s.sendUpdate(oneSecondTimeoutCtx, tv, "1")
|
|
|
|
// Process update in workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.Error(err)
|
|
s.IsType(err, (*serviceerror.NotFound)(nil))
|
|
s.ErrorContains(err, "Workflow task not found.")
|
|
|
|
updateResult := <-updateResultCh
|
|
s.Error(updateResult.err)
|
|
var notFound *serviceerror.NotFound
|
|
s.ErrorAs(updateResult.err, ¬Found)
|
|
s.Equal("workflow execution already completed", updateResult.err.Error())
|
|
s.Nil(updateResult.response)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT was converted to normal WT during termination.
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskFailed
|
|
8 WorkflowExecutionTerminated`, events)
|
|
|
|
msResp, err := s.adminClient.DescribeMutableState(NewContext(), &adminservice.DescribeMutableStateRequest{
|
|
Namespace: s.namespace,
|
|
Execution: tv.WorkflowExecution(),
|
|
})
|
|
s.NoError(err)
|
|
s.EqualValues(7, msResp.GetDatabaseMutableState().GetExecutionInfo().GetCompletionEventBatchId(), "completion_event_batch_id should point to WTFailed event")
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_ScheduledSpeculativeWorkflowTask_TerminateWorkflow() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
oneSecondTimeoutCtx, cancel := context.WithTimeout(NewContext(), 1*time.Second)
|
|
defer cancel()
|
|
updateResultCh := s.sendUpdate(oneSecondTimeoutCtx, tv, "1")
|
|
|
|
// Terminate workflow after speculative WT is scheduled but not started.
|
|
_, err = s.client.TerminateWorkflowExecution(NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
WorkflowExecution: tv.WorkflowExecution(),
|
|
Reason: tv.Any().String(),
|
|
})
|
|
s.NoError(err)
|
|
|
|
updateResult := <-updateResultCh
|
|
s.Error(updateResult.err)
|
|
var notFound *serviceerror.NotFound
|
|
s.ErrorAs(updateResult.err, ¬Found)
|
|
s.Equal("workflow execution already completed", updateResult.err.Error())
|
|
s.Nil(updateResult.response)
|
|
|
|
s.Equal(1, wtHandlerCalls)
|
|
s.Equal(1, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionTerminated // Speculative WTScheduled event is not written to history if WF is terminated.
|
|
`, events)
|
|
|
|
msResp, err := s.adminClient.DescribeMutableState(NewContext(), &adminservice.DescribeMutableStateRequest{
|
|
Namespace: s.namespace,
|
|
Execution: tv.WorkflowExecution(),
|
|
})
|
|
s.NoError(err)
|
|
s.EqualValues(5, msResp.GetDatabaseMutableState().GetExecutionInfo().GetCompletionEventBatchId(), "completion_event_batch_id should point to WFTerminated event")
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_CompleteWorkflow_AbortUpdates() {
|
|
type testCase struct {
|
|
Name string
|
|
Description string
|
|
UpdateErr string
|
|
UpdateFailure string
|
|
Commands func(tv *testvars.TestVars) []*commandpb.Command
|
|
Messages func(tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message
|
|
}
|
|
testCases := []testCase{
|
|
{
|
|
Name: "admitted",
|
|
Description: "update in stateAdmitted must get an error",
|
|
UpdateErr: "workflow execution already completed",
|
|
UpdateFailure: "",
|
|
Commands: func(_ *testvars.TestVars) []*commandpb.Command { return nil },
|
|
Messages: func(_ *testvars.TestVars, _ *protocolpb.Message) []*protocolpb.Message { return nil },
|
|
},
|
|
{
|
|
Name: "accepted",
|
|
Description: "update in stateAccepted must get an error",
|
|
UpdateErr: "workflow execution already completed",
|
|
UpdateFailure: "",
|
|
Commands: func(tv *testvars.TestVars) []*commandpb.Command { return s.UpdateAcceptCommands(tv, "1") },
|
|
Messages: func(tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message {
|
|
return s.UpdateAcceptMessages(tv, updRequestMsg, "1")
|
|
},
|
|
},
|
|
{
|
|
Name: "completed",
|
|
Description: "completed update must not be affected by workflow completion",
|
|
UpdateErr: "",
|
|
UpdateFailure: "",
|
|
Commands: func(tv *testvars.TestVars) []*commandpb.Command { return s.UpdateAcceptCompleteCommands(tv, "1") },
|
|
Messages: func(tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message {
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1")
|
|
},
|
|
},
|
|
{
|
|
Name: "rejected",
|
|
Description: "rejected update must be rejected with rejection from workflow",
|
|
UpdateErr: "",
|
|
UpdateFailure: "rejection-of-", // Rejection from workflow.
|
|
Commands: func(tv *testvars.TestVars) []*commandpb.Command { return nil },
|
|
Messages: func(tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message {
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
return append(tc.Commands(tv), &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
return tc.Messages(tv, updRequestMsg), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
halfSecondTimeoutCtx, cancel := context.WithTimeout(NewContext(), 500*time.Millisecond)
|
|
defer cancel()
|
|
updateResultCh := s.sendUpdate(halfSecondTimeoutCtx, tv, "1")
|
|
|
|
// Complete workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResult := <-updateResultCh
|
|
if tc.UpdateErr != "" {
|
|
s.Error(updateResult.err, tc.Description)
|
|
s.Contains(updateResult.err.Error(), tc.UpdateErr, tc.Description)
|
|
} else {
|
|
s.NoError(updateResult.err, tc.Description)
|
|
}
|
|
|
|
if tc.UpdateFailure != "" {
|
|
s.NotNil(updateResult.response.GetOutcome().GetFailure(), tc.Description)
|
|
s.Contains(updateResult.response.GetOutcome().GetFailure().GetMessage(), tc.UpdateFailure, tc.Description)
|
|
} else {
|
|
s.Nil(updateResult.response.GetOutcome().GetFailure(), tc.Description)
|
|
}
|
|
|
|
// Check that update didn't block workflow completion.
|
|
descResp, err := s.client.DescribeWorkflowExecution(NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
Execution: tv.WorkflowExecution(),
|
|
})
|
|
s.NoError(err)
|
|
s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, descResp.WorkflowExecutionInfo.Status)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_Heartbeat() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Events (5 and 6) are for speculative WT, but they won't disappear after reject because speculative WT is converted to normal during heartbeat.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
// Heartbeat from speculative WT (no messages, no commands).
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowTaskScheduled // New WT (after heartbeat) is normal and won't disappear from the history after reject.
|
|
9 WorkflowTaskStarted
|
|
`, task.History)
|
|
// Reject update.
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
var updRequestMsg *protocolpb.Message
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Len(task.Messages, 1)
|
|
updRequestMsg = task.Messages[0]
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
return nil, nil
|
|
case 3:
|
|
s.Empty(task.Messages)
|
|
return s.UpdateRejectMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Heartbeat from workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries, WithForceNewWorkflowTask)
|
|
s.NoError(err)
|
|
heartbeatResp := res.NewTask
|
|
|
|
// Reject update from workflow.
|
|
updateResp, err := poller.HandlePartialWorkflowTask(heartbeatResp.GetWorkflowTask(), false)
|
|
s.NoError(err)
|
|
s.NotNil(updateResp)
|
|
updateResult := <-updateResultCh
|
|
s.Equal("rejection-of-"+tv.UpdateID("1"), updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(0, updateResp.ResetHistoryEventId, "no reset of event ID should happened after update rejection because of heartbeat")
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted // Heartbeat response.
|
|
8 WorkflowTaskScheduled
|
|
9 WorkflowTaskStarted
|
|
10 WorkflowTaskCompleted // After heartbeat new normal WT was created and events are written into the history even update is rejected.
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_ScheduledSpeculativeWorkflowTask_LostUpdate() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Empty(task.Messages, "update lost due to lost update registry")
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
halfSecondTimeoutCtx, cancel := context.WithTimeout(NewContext(), 500*time.Millisecond)
|
|
defer cancel()
|
|
updateResult := <-s.sendUpdate(halfSecondTimeoutCtx, tv, "1")
|
|
s.Error(updateResult.err)
|
|
s.True(common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error())
|
|
s.Nil(updateResult.response)
|
|
|
|
// Lose update registry. Speculative WFT and update registry disappear.
|
|
s.loseUpdateRegistryAndAbandonPendingUpdates(tv)
|
|
|
|
// Ensure, there is no WFT.
|
|
pollCtx, cancel := context.WithTimeout(NewContext(), common.MinLongPollTimeout*2)
|
|
defer cancel()
|
|
pollResponse, err := s.client.PollWorkflowTaskQueue(pollCtx, &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
})
|
|
s.NoError(err)
|
|
s.Nil(pollResponse.Messages, "there should not be new WFT with messages")
|
|
|
|
// Send signal to schedule new WT.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Complete workflow and check that there is update messages.
|
|
completeWorkflowResp, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StartedSpeculativeWorkflowTask_LostUpdate() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT. Events 5 and 6 will be lost.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
|
|
// Lose update registry. Update is lost and NotFound error will be returned to RespondWorkflowTaskCompleted.
|
|
s.loseUpdateRegistryAndAbandonPendingUpdates(tv)
|
|
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
s.Empty(task.Messages, "no messages since update registry was lost")
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
halfSecondTimeoutCtx, cancel := context.WithTimeout(NewContext(), 500*time.Millisecond)
|
|
defer cancel()
|
|
updateResultCh := s.sendUpdate(halfSecondTimeoutCtx, tv, "1")
|
|
|
|
// Process update in workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.Error(err)
|
|
s.IsType(&serviceerror.NotFound{}, err)
|
|
s.ErrorContains(err, "Workflow task not found")
|
|
|
|
updateResult := <-updateResultCh
|
|
s.Error(updateResult.err)
|
|
s.True(common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error())
|
|
s.Nil(updateResult.response)
|
|
|
|
// Send signal to schedule new WFT.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
completeWorkflowResp, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionSignaled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted
|
|
8 WorkflowTaskCompleted
|
|
9 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_FirstNormalWorkflowTask_UpdateResurrectedAfterRegistryCleared() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
`, task.History)
|
|
// Clear update registry. Update will be resurrected in registry from acceptance message.
|
|
s.clearUpdateRegistryAndAbortPendingUpdates(tv)
|
|
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted
|
|
6 WorkflowExecutionUpdateCompleted
|
|
7 WorkflowExecutionSignaled
|
|
8 WorkflowTaskScheduled
|
|
9 WorkflowTaskStarted
|
|
`, task.History)
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
updRequestMsg := task.Messages[0]
|
|
s.EqualValues(2, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
case 2:
|
|
s.Empty(task.Messages, "update must be processed and not delivered again")
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow. Update won't be found on server but will be resurrected from acceptance message and completed.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
// Client receives resurrected Update outcome.
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
|
|
// Signal to create new WFT which shouldn't get any updates.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted
|
|
6 WorkflowExecutionUpdateCompleted
|
|
7 WorkflowExecutionSignaled
|
|
8 WorkflowTaskScheduled
|
|
9 WorkflowTaskStarted
|
|
10 WorkflowTaskCompleted
|
|
11 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_ScheduledSpeculativeWorkflowTask_DeduplicateID() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Len(task.Messages, 1, "2nd update must be deduplicated by ID")
|
|
updRequestMsg := task.Messages[0]
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Send second update with the same ID.
|
|
updateResultCh2 := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
updateResult2 := <-updateResultCh2
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult2.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, updateResp.ResetHistoryEventId)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker.
|
|
9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8}
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StartedSpeculativeWorkflowTask_DeduplicateID() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
var updateResultCh2 <-chan *workflowservice.UpdateWorkflowExecutionResponse
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
// Send second update with the same ID when WT is started but not completed.
|
|
updateResultCh2 = s.sendUpdateNoError(tv, "1")
|
|
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
s.Len(task.Messages, 1, "2nd update should not has reached server yet")
|
|
updRequestMsg := task.Messages[0]
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, res.NewTask.ResetHistoryEventId)
|
|
|
|
updateResult2 := <-updateResultCh2
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult2.GetOutcome().GetSuccess()))
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker.
|
|
9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8}
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_CompletedSpeculativeWorkflowTask_DeduplicateID() {
|
|
testCases := []struct {
|
|
Name string
|
|
CloseShard bool
|
|
}{
|
|
{
|
|
Name: "no shard reload",
|
|
CloseShard: false,
|
|
},
|
|
{
|
|
Name: "with shard reload",
|
|
CloseShard: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.Name, func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
case 3:
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
case 3:
|
|
s.Empty(task.Messages, "2nd update must be deduplicated by ID ")
|
|
return nil, nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process update in workflow.
|
|
_, err = poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
|
|
if tc.CloseShard {
|
|
// Close shard to make sure that for completed updates deduplication works even after shard reload.
|
|
s.closeShard(tv.WorkflowID())
|
|
}
|
|
|
|
// Send second update with the same ID. It must return immediately.
|
|
updateResult2 := <-s.sendUpdateNoError(tv, "1")
|
|
|
|
// Ensure, there is no new WT.
|
|
pollCtx, cancel := context.WithTimeout(NewContext(), common.MinLongPollTimeout*2)
|
|
defer cancel()
|
|
pollResponse, err := s.client.PollWorkflowTaskQueue(pollCtx, &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
})
|
|
s.NoError(err)
|
|
s.Nil(pollResponse.Messages, "there must be no new WT")
|
|
|
|
s.EqualValues(
|
|
"success-result-of-"+tv.UpdateID("1"),
|
|
decodeString(s, updateResult2.GetOutcome().GetSuccess()),
|
|
"results of the first update must be available")
|
|
|
|
// Send signal to schedule new WT.
|
|
err = s.sendSignal(s.namespace, tv.WorkflowExecution(), tv.Any().String(), tv.Any().Payloads(), tv.Any().String())
|
|
s.NoError(err)
|
|
|
|
// Complete workflow.
|
|
completeWorkflowResp, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
s.NotNil(completeWorkflowResp)
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker.
|
|
9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8}
|
|
10 WorkflowExecutionSignaled
|
|
11 WorkflowTaskScheduled
|
|
12 WorkflowTaskStarted
|
|
13 WorkflowTaskCompleted
|
|
14 WorkflowExecutionCompleted
|
|
`, events)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StaleSpeculativeWorkflowTask_Fail_BecauseOfDifferentStartedId() {
|
|
/*
|
|
Test scenario:
|
|
An update created a speculative WT and WT is dispatched to the worker (started).
|
|
Update registry is cleared, speculative WT disappears from server.
|
|
Update is retired and second speculative WT is scheduled but not dispatched yet.
|
|
An activity completes, it converts the 2nd speculative WT into normal one.
|
|
The first speculative WT responds back, server fails request because WorkflowTaskStarted event Id is mismatched.
|
|
The second speculative WT responds back and server completes it.
|
|
*/
|
|
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Schedule activity.
|
|
return []*commandpb.Command{{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID(),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: tv.InfiniteTimeout(),
|
|
}},
|
|
}}, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) {
|
|
return tv.Any().Payloads(), false, nil
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
ActivityTaskHandler: atHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// First WT will schedule activity.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
// Send 1st update. It will create 2nd WT as speculative.
|
|
s.sendUpdateNoError(tv, "1")
|
|
|
|
// Poll 2nd speculative WT with 1st update.
|
|
wt2, err := s.client.PollWorkflowTaskQueue(NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt2)
|
|
s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token")
|
|
s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update")
|
|
s.EqualValues(7, wt2.StartedEventId)
|
|
s.EqualValues(6, wt2.Messages[0].GetEventId())
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled
|
|
7 WorkflowTaskStarted`, wt2.History)
|
|
|
|
// Clear update registry. Speculative WFT disappears from server.
|
|
s.clearUpdateRegistryAndAbortPendingUpdates(tv)
|
|
|
|
// Wait for update request to be retry by frontend and recreated in registry. This will create a 3rd WFT as speculative.
|
|
s.waitUpdateAdmitted(tv, "1")
|
|
|
|
// Before polling for the 3rd speculative WT, process activity. This will convert 3rd speculative WT to normal WT.
|
|
err = poller.PollAndProcessActivityTask(false)
|
|
s.NoError(err)
|
|
|
|
// Poll the 3rd WFT (not speculative anymore) but must have 2nd update.
|
|
wt3, err := s.client.PollWorkflowTaskQueue(NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt3)
|
|
s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token")
|
|
s.Len(wt3.Messages, 1, "3rd workflow task must have a message with 2nd update")
|
|
s.EqualValues(9, wt3.StartedEventId)
|
|
s.EqualValues(8, wt3.Messages[0].GetEventId())
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled
|
|
7 ActivityTaskStarted
|
|
8 ActivityTaskCompleted
|
|
9 WorkflowTaskStarted`, wt3.History)
|
|
|
|
// Now try to complete 2nd WT (speculative). It should fail because WorkflowTaskStarted event Id is mismatched.
|
|
_, err = s.client.RespondWorkflowTaskCompleted(NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt2.TaskToken,
|
|
Commands: s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
Messages: s.UpdateAcceptCompleteMessages(tv, wt2.Messages[0], "1"),
|
|
})
|
|
s.Error(err, "Must fail because WorkflowTaskStarted event Id is different.")
|
|
s.IsType(&serviceerror.NotFound{}, err)
|
|
s.Contains(err.Error(), "Workflow task not found")
|
|
|
|
// Complete 3rd WT. It should succeed.
|
|
_, err = s.client.RespondWorkflowTaskCompleted(NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt3.TaskToken,
|
|
Commands: s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
Messages: s.UpdateAcceptCompleteMessages(tv, wt3.Messages[0], "1"),
|
|
})
|
|
s.NoError(err)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 ActivityTaskScheduled
|
|
6 WorkflowTaskScheduled
|
|
7 ActivityTaskStarted {"ScheduledEventId":5}
|
|
8 ActivityTaskCompleted
|
|
9 WorkflowTaskStarted
|
|
10 WorkflowTaskCompleted
|
|
11 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId":8}
|
|
12 WorkflowExecutionUpdateCompleted
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StaleSpeculativeWorkflowTask_Fail_BecauseOfDifferentStartTime() {
|
|
/*
|
|
Test scenario:
|
|
An update created a speculative WT and WT is dispatched to the worker (started).
|
|
WF context is cleared, speculative WT is disappeared from server.
|
|
Update is retried and second speculative WT is dispatched to worker with same WT scheduled/started Id and update Id.
|
|
The first speculative WT respond back, server reject it because startTime is different.
|
|
The second speculative WT respond back, server accept it.
|
|
*/
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// First WT will schedule activity.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
|
|
// Send update. It will create 2nd WT as speculative.
|
|
s.sendUpdateNoError(tv, "1")
|
|
|
|
// Poll 2nd speculative WT with 1st update.
|
|
wt2, err := s.client.PollWorkflowTaskQueue(NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt2)
|
|
s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token")
|
|
s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update")
|
|
s.EqualValues(6, wt2.StartedEventId)
|
|
s.EqualValues(5, wt2.Messages[0].GetEventId())
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted`, wt2.History)
|
|
|
|
// Clear update registry. Speculative WFT disappears from server.
|
|
s.clearUpdateRegistryAndAbortPendingUpdates(tv)
|
|
|
|
// Wait for update request to be retry by frontend and recreated in registry. This will create a 3rd WFT as speculative.
|
|
s.waitUpdateAdmitted(tv, "1")
|
|
|
|
// Poll for the 3rd speculative WT.
|
|
wt3, err := s.client.PollWorkflowTaskQueue(NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt3)
|
|
s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token")
|
|
s.Len(wt3.Messages, 1, "3rd workflow task must have a message with 1st update")
|
|
s.EqualValues(6, wt3.StartedEventId)
|
|
s.EqualValues(5, wt3.Messages[0].GetEventId())
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted`, wt3.History)
|
|
|
|
// Now try to complete 2nd (speculative) WT, it should fail.
|
|
_, err = s.client.RespondWorkflowTaskCompleted(NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt2.TaskToken,
|
|
Commands: s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
Messages: s.UpdateAcceptCompleteMessages(tv, wt2.Messages[0], "1"),
|
|
})
|
|
s.Error(err, "Must fail because workflow task start time is different.")
|
|
s.IsType(&serviceerror.NotFound{}, err)
|
|
s.Contains(err.Error(), "Workflow task not found")
|
|
|
|
// Try to complete 3rd WT, it should succeed
|
|
_, err = s.client.RespondWorkflowTaskCompleted(NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt3.TaskToken,
|
|
Commands: s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
Messages: s.UpdateAcceptCompleteMessages(tv, wt3.Messages[0], "1"),
|
|
})
|
|
s.NoError(err, "2nd speculative WT should be completed because it has same WT scheduled/started Id and startTime matches the accepted message is valid (same update Id)")
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId":5}
|
|
9 WorkflowExecutionUpdateCompleted
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_StaleSpeculativeWorkflowTask_Fail_NewWorkflowTaskWith2Updates() {
|
|
/*
|
|
Test scenario:
|
|
An update created a speculative WT and WT is dispatched to the worker (started).
|
|
Mutable state cleared, speculative WT and update registry are disappeared from server.
|
|
First update is retried and another update come in.
|
|
Second speculative WT is dispatched to worker with same WT scheduled/started Id but 2 updates.
|
|
The first speculative WT responds back, server rejected it (different start time).
|
|
The second speculative WT responds back, server accepted it.
|
|
*/
|
|
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
testCtx := NewContext()
|
|
|
|
// Drain first WFT.
|
|
wt1, err := s.client.PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt1)
|
|
_, err = s.client.RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt1.TaskToken,
|
|
})
|
|
s.NoError(err)
|
|
|
|
// Send 1st update. It will create 2nd speculative WFT.
|
|
s.sendUpdateNoError(tv, "1")
|
|
|
|
// Poll 2nd speculative WFT with 1st update.
|
|
wt2, err := s.client.PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt2)
|
|
s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token")
|
|
s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update")
|
|
s.EqualValues(6, wt2.StartedEventId)
|
|
s.EqualValues(5, wt2.Messages[0].GetEventId())
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted`, wt2.History)
|
|
|
|
// Clear update registry. Speculative WFT disappears from server.
|
|
s.clearUpdateRegistryAndAbortPendingUpdates(tv)
|
|
|
|
// Make sure UpdateWorkflowExecution call for the update "1" is retried and new (3rd) WFT is created as speculative with updateID=1.
|
|
s.waitUpdateAdmitted(tv, "1")
|
|
|
|
// Send 2nd update (with DIFFERENT updateId). It reuses already created 3rd WFT.
|
|
s.sendUpdateNoError(tv, "2")
|
|
// updateID=1 is still blocked. There must be 2 blocked updates now.
|
|
|
|
// Poll the 3rd speculative WFT.
|
|
wt3, err := s.client.PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt3)
|
|
s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token")
|
|
s.Len(wt3.Messages, 2, "3rd workflow task must have a message with 1st and 2nd updates")
|
|
s.EqualValues(6, wt3.StartedEventId)
|
|
s.EqualValues(5, wt3.Messages[0].GetEventId())
|
|
s.EqualValues(5, wt3.Messages[1].GetEventId())
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted`, wt3.History)
|
|
|
|
// Now try to complete 2nd speculative WT, it should fail because start time does not match.
|
|
_, err = s.client.RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt2.TaskToken,
|
|
Commands: s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
Messages: s.UpdateAcceptCompleteMessages(tv, wt2.Messages[0], "1"),
|
|
ReturnNewWorkflowTask: true,
|
|
})
|
|
s.Error(err, "Must fail because start time is different.")
|
|
s.Contains(err.Error(), "Workflow task not found")
|
|
s.IsType(&serviceerror.NotFound{}, err)
|
|
|
|
// Complete of the 3rd WT should succeed. It must accept both updates.
|
|
wt4Resp, err := s.client.RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Namespace: s.namespace,
|
|
TaskToken: wt3.TaskToken,
|
|
Commands: append(
|
|
s.UpdateAcceptCompleteCommands(tv, "1"),
|
|
s.UpdateAcceptCompleteCommands(tv, "2")...),
|
|
Messages: append(
|
|
s.UpdateAcceptCompleteMessages(tv, wt3.Messages[0], "1"),
|
|
s.UpdateAcceptCompleteMessages(tv, wt3.Messages[1], "2")...),
|
|
ReturnNewWorkflowTask: true,
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(wt4Resp)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5}
|
|
9 WorkflowExecutionUpdateCompleted
|
|
10 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5}
|
|
11 WorkflowExecutionUpdateCompleted
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_WorkerSkippedProcessing_RejectByServer() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
var update2ResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Speculative WT.
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
update2ResultCh = s.sendUpdateNoError(tv, "2")
|
|
return nil, nil
|
|
case 3:
|
|
s.EqualHistory(`
|
|
4 WorkflowTaskCompleted // Speculative WT was dropped and history starts from 4 again.
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted`, task.History)
|
|
commands := append(s.UpdateAcceptCompleteCommands(tv, "2"),
|
|
&commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}},
|
|
})
|
|
return commands, nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
// Don't process update in WT.
|
|
return nil, nil
|
|
case 3:
|
|
s.Len(task.Messages, 1)
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("2"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "2"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Identity: "old_worker",
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
// Process 2nd WT which ignores update message.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
s.Equal("Workflow Update is rejected because it wasn't processed by worker. Probably, Workflow Update is not supported by the worker.", updateResult.GetOutcome().GetFailure().GetMessage())
|
|
s.EqualValues(3, updateResp.ResetHistoryEventId)
|
|
|
|
// Process 3rd WT which completes 2nd update and workflow.
|
|
update2Resp, err := poller.HandlePartialWorkflowTask(updateResp.GetWorkflowTask(), false)
|
|
s.NoError(err)
|
|
s.NotNil(update2Resp)
|
|
update2Result := <-update2ResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("2"), decodeString(s, update2Result.GetOutcome().GetSuccess()))
|
|
|
|
s.Equal(3, wtHandlerCalls)
|
|
s.Equal(3, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // 1st speculative WT is not present in the history. This is 2nd speculative WT.
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted
|
|
8 WorkflowExecutionUpdateAccepted
|
|
9 WorkflowExecutionUpdateCompleted
|
|
10 WorkflowExecutionCompleted`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_LastWorkflowTask_HasUpdateMessage() {
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
completeWorkflowCommand := &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION,
|
|
Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{
|
|
CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{
|
|
Result: tv.Any().Payloads(),
|
|
},
|
|
},
|
|
}
|
|
return append(s.UpdateAcceptCommands(tv, "1"), completeWorkflowCommand), nil
|
|
},
|
|
MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
return s.UpdateAcceptMessages(tv, task.Messages[0], "1"), nil
|
|
},
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
s.sendUpdateNoErrorWaitPolicyAccepted(tv, tv.UpdateID())
|
|
pollResponse := make(chan error)
|
|
go func() {
|
|
// Blocks until the update request causes a WFT to be dispatched; then sends the update complete message
|
|
// required for the update request to return.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
pollResponse <- err
|
|
}()
|
|
s.NoError(<-pollResponse)
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowExecutionUpdateAccepted
|
|
6 WorkflowExecutionCompleted
|
|
`, s.getHistory(s.namespace, tv.WorkflowExecution()))
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_SpeculativeWorkflowTask_QueryFailureClearsWFContext() {
|
|
tv := testvars.New(s.T())
|
|
|
|
tv = s.startWorkflow(tv)
|
|
|
|
wtHandlerCalls := 0
|
|
wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
wtHandlerCalls++
|
|
switch wtHandlerCalls {
|
|
case 1:
|
|
// Completes first WT with empty command list.
|
|
return nil, nil
|
|
case 2:
|
|
s.EqualHistory(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled
|
|
6 WorkflowTaskStarted
|
|
`, task.History)
|
|
return s.UpdateAcceptCompleteCommands(tv, "1"), nil
|
|
default:
|
|
s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
msgHandlerCalls := 0
|
|
msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
msgHandlerCalls++
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return nil, nil
|
|
case 2:
|
|
updRequestMsg := task.Messages[0]
|
|
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
|
|
|
|
s.Equal("args-value-of-"+tv.UpdateID("1"), decodeString(s, updRequest.GetInput().GetArgs()))
|
|
s.Equal(tv.HandlerName(), updRequest.GetInput().GetName())
|
|
s.EqualValues(5, updRequestMsg.GetEventId())
|
|
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
WorkflowTaskHandler: wtHandler,
|
|
MessageHandler: msgHandler,
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
// Drain first WT.
|
|
_, err := poller.PollAndProcessWorkflowTask()
|
|
s.NoError(err)
|
|
|
|
updateResultCh := s.sendUpdateNoError(tv, "1")
|
|
|
|
type QueryResult struct {
|
|
Resp *workflowservice.QueryWorkflowResponse
|
|
Err error
|
|
}
|
|
queryFn := func(resCh chan<- QueryResult) {
|
|
// There is no query handler, and query timeout is ok for this test.
|
|
// But first query must not time out before 2nd query reached server,
|
|
// because 2 queries overflow the query buffer (default size 1),
|
|
// which leads to clearing of WF context.
|
|
shortCtx, cancel := context.WithTimeout(NewContext(), 100*time.Millisecond)
|
|
defer cancel()
|
|
queryResp, err := s.client.QueryWorkflow(shortCtx, &workflowservice.QueryWorkflowRequest{
|
|
Namespace: s.namespace,
|
|
Execution: tv.WorkflowExecution(),
|
|
Query: &querypb.WorkflowQuery{
|
|
QueryType: tv.Any().String(),
|
|
},
|
|
})
|
|
resCh <- QueryResult{Resp: queryResp, Err: err}
|
|
}
|
|
|
|
query1ResultCh := make(chan QueryResult)
|
|
query2ResultCh := make(chan QueryResult)
|
|
go queryFn(query1ResultCh)
|
|
go queryFn(query2ResultCh)
|
|
query1Res := <-query1ResultCh
|
|
query2Res := <-query2ResultCh
|
|
s.Error(query1Res.Err)
|
|
s.Error(query2Res.Err)
|
|
s.Nil(query1Res.Resp)
|
|
s.Nil(query2Res.Resp)
|
|
|
|
var queryBufferFullErr *serviceerror.ResourceExhausted
|
|
if common.IsContextDeadlineExceededErr(query1Res.Err) {
|
|
s.True(common.IsContextDeadlineExceededErr(query1Res.Err), "one of query errors must be CDE")
|
|
s.ErrorAs(query2Res.Err, &queryBufferFullErr, "one of query errors must `query buffer is full`")
|
|
s.Contains(query2Res.Err.Error(), "query buffer is full", "one of query errors must `query buffer is full`")
|
|
} else {
|
|
s.ErrorAs(query1Res.Err, &queryBufferFullErr, "one of query errors must `query buffer is full`")
|
|
s.Contains(query1Res.Err.Error(), "query buffer is full", "one of query errors must `query buffer is full`")
|
|
s.True(common.IsContextDeadlineExceededErr(query2Res.Err), "one of query errors must be CDE")
|
|
}
|
|
|
|
// "query buffer is full" error clears WF context. If update registry is not cleared together with context (old behaviour),
|
|
// then update stays there but speculative WFT which supposed to deliver it, is cleared.
|
|
// Subsequent retry attempts of "UpdateWorkflowExecution" API wouldn't help, because update is deduped by registry,
|
|
// and new WFT is not created. Update is not delivered to the worker until new WFT is created.
|
|
// If registry is cleared together with WF context (current behaviour), retries of "UpdateWorkflowExecution"
|
|
// will create new update and WFT.
|
|
|
|
// Wait to make sure that UpdateWorkflowExecution call is retried, update and speculative WFT are recreated.
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Process update in workflow.
|
|
res, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
updateResp := res.NewTask
|
|
updateResult := <-updateResultCh
|
|
s.EqualValues("success-result-of-"+tv.UpdateID("1"), decodeString(s, updateResult.GetOutcome().GetSuccess()))
|
|
s.EqualValues(0, updateResp.ResetHistoryEventId)
|
|
|
|
s.Equal(2, wtHandlerCalls)
|
|
s.Equal(2, msgHandlerCalls)
|
|
|
|
events := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
|
|
s.EqualHistoryEvents(`
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
5 WorkflowTaskScheduled // Was speculative WT...
|
|
6 WorkflowTaskStarted
|
|
7 WorkflowTaskCompleted // ...and events were written to the history when WT completes.
|
|
8 WorkflowExecutionUpdateAccepted
|
|
9 WorkflowExecutionUpdateCompleted
|
|
`, events)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWorkflow_UpdatesAreSentToWorkerInOrderOfAdmission() {
|
|
// If our implementation is not in fact ordering updates correctly, then it may be ordering them
|
|
// non-deterministically. This number should be high enough that the false-negative rate of the test is low, but
|
|
// must not exceed our limit on number of in-flight updates. If we were picking a random ordering then the
|
|
// false-negative rate would be 1/(nUpdates!).
|
|
nUpdates := 10
|
|
|
|
tv := testvars.New(s.T())
|
|
tv = s.startWorkflow(tv)
|
|
for i := 0; i < nUpdates; i++ {
|
|
// Sequentially send updates one by one.
|
|
updateId := strconv.Itoa(i)
|
|
s.sendUpdateNoError(tv, updateId)
|
|
}
|
|
|
|
wtHandlerCalls := 0
|
|
msgHandlerCalls := 0
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
msgHandlerCalls++
|
|
var commands []*commandpb.Command
|
|
for i := range task.Messages {
|
|
commands = append(commands, s.UpdateAcceptCompleteCommands(tv, strconv.Itoa(i))...)
|
|
}
|
|
return commands, nil
|
|
},
|
|
MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
wtHandlerCalls++
|
|
s.Len(task.Messages, nUpdates)
|
|
var messages []*protocolpb.Message
|
|
// Updates were sent in sequential order of updateId => messages must be ordered in the same way.
|
|
for i, m := range task.Messages {
|
|
s.Equal(tv.UpdateID(strconv.Itoa(i)), m.ProtocolInstanceId)
|
|
messages = append(messages, s.UpdateAcceptCompleteMessages(tv, m, strconv.Itoa(i))...)
|
|
}
|
|
return messages, nil
|
|
},
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
_, err := poller.PollAndProcessWorkflowTask(WithoutRetries)
|
|
s.NoError(err)
|
|
s.Equal(1, wtHandlerCalls)
|
|
s.Equal(1, msgHandlerCalls)
|
|
|
|
expectedHistory := `
|
|
1 WorkflowExecutionStarted
|
|
2 WorkflowTaskScheduled
|
|
3 WorkflowTaskStarted
|
|
4 WorkflowTaskCompleted
|
|
`
|
|
for i := 0; i < nUpdates; i++ {
|
|
expectedHistory += fmt.Sprintf(`
|
|
%d WorkflowExecutionUpdateAccepted {"AcceptedRequest":{"Meta": {"UpdateId": "%s"}}}
|
|
%d WorkflowExecutionUpdateCompleted {"Meta": {"UpdateId": "%s"}}`,
|
|
5+2*i, tv.UpdateID(strconv.Itoa(i)),
|
|
6+2*i, tv.UpdateID(strconv.Itoa(i)))
|
|
}
|
|
|
|
history := s.getHistory(s.namespace, tv.WorkflowExecution())
|
|
s.EqualHistoryEvents(expectedHistory, history)
|
|
}
|
|
|
|
func (s *FunctionalSuite) TestUpdateWithStart() {
|
|
// reset reuse minimal interval to allow workflow termination
|
|
s.OverrideDynamicConfig(dynamicconfig.WorkflowIdReuseMinimalInterval, 0)
|
|
|
|
type multiopsResponseErr struct {
|
|
response *workflowservice.ExecuteMultiOperationResponse
|
|
err error
|
|
}
|
|
|
|
runMultiOp := func(
|
|
tv *testvars.TestVars,
|
|
request *workflowservice.ExecuteMultiOperationRequest,
|
|
) (*workflowservice.ExecuteMultiOperationResponse, error) {
|
|
capture := s.testCluster.host.captureMetricsHandler.StartCapture()
|
|
defer s.testCluster.host.captureMetricsHandler.StopCapture(capture)
|
|
|
|
msgHandlerCalls := 0
|
|
poller := &TaskPoller{
|
|
Client: s.client,
|
|
Namespace: s.namespace,
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) {
|
|
return nil, nil
|
|
},
|
|
MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) {
|
|
if len(task.Messages) > 0 {
|
|
updRequestMsg := task.Messages[0]
|
|
msgHandlerCalls += 1
|
|
switch msgHandlerCalls {
|
|
case 1:
|
|
return s.UpdateAcceptCompleteMessages(tv, updRequestMsg, "1"), nil
|
|
default:
|
|
s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls)
|
|
}
|
|
}
|
|
return nil, nil
|
|
},
|
|
Logger: s.Logger,
|
|
T: s.T(),
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
retCh := make(chan multiopsResponseErr)
|
|
go func() {
|
|
resp, err := s.client.ExecuteMultiOperation(ctx, request)
|
|
retCh <- multiopsResponseErr{resp, err}
|
|
}()
|
|
|
|
go func() {
|
|
// TODO: handle error
|
|
_, _ = poller.PollAndProcessWorkflowTask(WithDumpHistory)
|
|
}()
|
|
|
|
ret := <-retCh
|
|
return ret.response, ret.err
|
|
}
|
|
|
|
runUpdateWithStart := func(
|
|
tv *testvars.TestVars,
|
|
startReq *workflowservice.StartWorkflowExecutionRequest,
|
|
updateReq *workflowservice.UpdateWorkflowExecutionRequest,
|
|
) (*workflowservice.ExecuteMultiOperationResponse, error) {
|
|
resp, err := runMultiOp(tv,
|
|
&workflowservice.ExecuteMultiOperationRequest{
|
|
Namespace: s.namespace,
|
|
Operations: []*workflowservice.ExecuteMultiOperationRequest_Operation{
|
|
{
|
|
Operation: &workflowservice.ExecuteMultiOperationRequest_Operation_StartWorkflow{
|
|
StartWorkflow: startReq,
|
|
},
|
|
},
|
|
{
|
|
Operation: &workflowservice.ExecuteMultiOperationRequest_Operation_UpdateWorkflow{
|
|
UpdateWorkflow: updateReq,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if err == nil {
|
|
s.Len(resp.Responses, 2)
|
|
|
|
startRes := resp.Responses[0].Response.(*workflowservice.ExecuteMultiOperationResponse_Response_StartWorkflow).StartWorkflow
|
|
s.NotZero(startRes.RunId)
|
|
|
|
updateRes := resp.Responses[1].Response.(*workflowservice.ExecuteMultiOperationResponse_Response_UpdateWorkflow).UpdateWorkflow
|
|
s.NotNil(updateRes.Outcome)
|
|
s.NotZero(updateRes.Outcome.String())
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
startWorkflowReq := func(tv *testvars.TestVars) *workflowservice.StartWorkflowExecutionRequest {
|
|
return &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
WorkflowId: tv.WorkflowID(),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
Identity: tv.WorkerIdentity(),
|
|
}
|
|
}
|
|
|
|
updateWorkflowReq := func(tv *testvars.TestVars) *workflowservice.UpdateWorkflowExecutionRequest {
|
|
return &workflowservice.UpdateWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
Request: &updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: tv.UpdateID("1")},
|
|
Input: &updatepb.Input{Name: tv.Any().String(), Args: tv.Any().Payloads()},
|
|
},
|
|
WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: tv.WorkflowID()},
|
|
WaitPolicy: &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED},
|
|
}
|
|
}
|
|
|
|
s.Run("workflow is not running", func() {
|
|
|
|
s.Run("start workflow and send update", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
resp, err := runUpdateWithStart(tv, startWorkflowReq(tv), updateWorkflowReq(tv))
|
|
s.NoError(err)
|
|
s.True(resp.Responses[0].GetStartWorkflow().Started)
|
|
})
|
|
|
|
s.Run("poll update result after completion", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
_, err := runUpdateWithStart(tv, startWorkflowReq(tv), updateWorkflowReq(tv))
|
|
s.NoError(err)
|
|
|
|
_, err = s.pollUpdate(tv, "1",
|
|
&updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED})
|
|
s.Nil(err)
|
|
})
|
|
|
|
s.Run("workflow id conflict policy terminate-existing: not supported yet", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
req := startWorkflowReq(tv)
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING
|
|
_, err := runUpdateWithStart(tv, req, updateWorkflowReq(tv))
|
|
s.Error(err)
|
|
})
|
|
})
|
|
|
|
s.Run("workflow is running", func() {
|
|
|
|
s.Run("workflow id conflict policy use-existing: only send update", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
_, err := s.client.StartWorkflowExecution(NewContext(), startWorkflowReq(tv))
|
|
s.NoError(err)
|
|
|
|
req := startWorkflowReq(tv)
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING
|
|
resp, err := runUpdateWithStart(tv, req, updateWorkflowReq(tv))
|
|
s.NoError(err)
|
|
s.False(resp.Responses[0].GetStartWorkflow().Started)
|
|
})
|
|
|
|
s.Run("workflow id conflict policy terminate-existing: terminate workflow first, then start and update", func() {
|
|
s.T().Skip("TODO")
|
|
tv := testvars.New(s.T())
|
|
|
|
initReq := startWorkflowReq(tv)
|
|
initReq.TaskQueue.Name = initReq.TaskQueue.Name + "-init" // avoid race condition with poller
|
|
initWF, err := s.client.StartWorkflowExecution(NewContext(), initReq)
|
|
s.NoError(err)
|
|
|
|
req := startWorkflowReq(tv)
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING
|
|
resp, err := runUpdateWithStart(tv, req, updateWorkflowReq(tv))
|
|
s.NoError(err)
|
|
s.True(resp.Responses[0].GetStartWorkflow().Started)
|
|
|
|
descResp, err := s.client.DescribeWorkflowExecution(NewContext(),
|
|
&workflowservice.DescribeWorkflowExecutionRequest{
|
|
Namespace: s.namespace,
|
|
Execution: &commonpb.WorkflowExecution{WorkflowId: req.WorkflowId, RunId: initWF.RunId},
|
|
})
|
|
s.NoError(err)
|
|
s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, descResp.WorkflowExecutionInfo.Status)
|
|
})
|
|
|
|
s.Run("workflow id conflict policy fail: abort multi operation", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
_, err := s.client.StartWorkflowExecution(NewContext(), startWorkflowReq(tv))
|
|
s.NoError(err)
|
|
|
|
req := startWorkflowReq(tv)
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL
|
|
_, err = runUpdateWithStart(tv, req, updateWorkflowReq(tv))
|
|
s.Error(err)
|
|
s.Equal(err.Error(), "MultiOperation could not be executed.")
|
|
errs := err.(*serviceerror.MultiOperationExecution).OperationErrors()
|
|
s.Len(errs, 2)
|
|
s.Contains(errs[0].Error(), "Workflow execution is already running")
|
|
s.Equal("Operation was aborted.", errs[1].Error())
|
|
})
|
|
|
|
s.Run("poll update result after completion", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
_, err := s.client.StartWorkflowExecution(NewContext(), startWorkflowReq(tv))
|
|
s.NoError(err)
|
|
|
|
req := startWorkflowReq(tv)
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING
|
|
_, err = runUpdateWithStart(tv, req, updateWorkflowReq(tv))
|
|
s.NoError(err)
|
|
|
|
_, err = s.pollUpdate(tv, "1",
|
|
&updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED})
|
|
s.Nil(err)
|
|
})
|
|
})
|
|
|
|
s.Run("dedupes both operations", func() {
|
|
|
|
s.Run("for workflow id conflict policy fail", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
startReq := startWorkflowReq(tv)
|
|
startReq.RequestId = "request_id"
|
|
startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL
|
|
updReq := updateWorkflowReq(tv)
|
|
|
|
resp1, err := runUpdateWithStart(tv, startReq, updReq)
|
|
s.NoError(err)
|
|
|
|
resp2, err := runUpdateWithStart(tv, startReq, updReq)
|
|
s.NoError(err)
|
|
|
|
s.Equal(resp1.Responses[0].GetStartWorkflow().RunId, resp2.Responses[0].GetStartWorkflow().RunId)
|
|
s.Equal(resp1.Responses[1].GetUpdateWorkflow().Outcome.GetSuccess(), resp2.Responses[1].GetUpdateWorkflow().Outcome.GetSuccess())
|
|
})
|
|
|
|
s.Run("for workflow id conflict policy use existing", func() {
|
|
tv := testvars.New(s.T())
|
|
|
|
startReq := startWorkflowReq(tv)
|
|
startReq.RequestId = "request_id"
|
|
startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING
|
|
updReq := updateWorkflowReq(tv)
|
|
|
|
resp1, err := runUpdateWithStart(tv, startReq, updReq)
|
|
s.NoError(err)
|
|
|
|
resp2, err := runUpdateWithStart(tv, startReq, updReq)
|
|
s.NoError(err)
|
|
|
|
s.Equal(resp1.Responses[0].GetStartWorkflow().RunId, resp2.Responses[0].GetStartWorkflow().RunId)
|
|
s.Equal(resp1.Responses[1].GetUpdateWorkflow().Outcome.GetSuccess(), resp2.Responses[1].GetUpdateWorkflow().Outcome.GetSuccess())
|
|
})
|
|
|
|
s.Run("for workflow id conflict policy terminate", func() {
|
|
s.T().Skip("TODO")
|
|
tv := testvars.New(s.T())
|
|
|
|
startReq := startWorkflowReq(tv)
|
|
startReq.RequestId = "request_id"
|
|
startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING
|
|
updReq := updateWorkflowReq(tv)
|
|
|
|
resp1, err := runUpdateWithStart(tv, startReq, updReq)
|
|
s.NoError(err)
|
|
|
|
resp2, err := runUpdateWithStart(tv, startReq, updReq)
|
|
s.NoError(err)
|
|
|
|
s.Equal(resp1.Responses[0].GetStartWorkflow().RunId, resp2.Responses[0].GetStartWorkflow().RunId)
|
|
s.Equal(resp1.Responses[1].GetUpdateWorkflow().Outcome.GetSuccess(), resp2.Responses[1].GetUpdateWorkflow().Outcome.GetSuccess())
|
|
})
|
|
})
|
|
}
|