Files
tasks/tests/pause_workflow_execution_test.go
Chetan GowdaandGitHub 2312f6752f Prevent workflow task and activity task generation when workflow is paused (#8687)
## What changed?
- Prevents workflow task generation when paused (ex: signals, activity
completions, child workflow completions etc)
- Prevents activity task generation when a workflow is paused and an
inflight workflow task comes back with start activity commands.
- Prevents eager activity starts when the workflow is paused.

## Why?
When a workflow is paused, we should not generate any more new activity
and workflow tasks.

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


---

> [!NOTE]
> Prevents workflow/activity task creation and certain API operations
when a workflow is paused, adds safeguards and tests across history,
NDC, and task refresh paths.
> 
> - **Behavior/State**:
> - Add `MutableState.IsWorkflowExecutionStatusPaused()` and honor
paused status across scheduling paths.
>   - `workflow/util.ScheduleWorkflowTask` returns early if paused.
> - **API Handlers**:
> - `RespondWorkflowTaskCompleted`: reject `ForceCreateNewWorkflowTask`
when paused; block eager activity and bypass activity task gen when
paused.
> - `SignalWithStartWorkflow`: do not schedule a new workflow task if
paused.
>   - `UpdateWorkflow`: reject updates while paused.
>   - Activity failure/respond paths: avoid new WFT when paused.
> - **NDC/Replication**:
> - Reapply events and buffered-event flusher: skip scheduling WFT if
paused.
> - `ndc.Workflow.FlushBufferedEvents`: don't schedule WFT when paused.
> - **Task Refresher**:
> - Skip refreshing WFT/activity tasks when paused; allow
timers/search-attr updates; skip close tasks for running/paused.
> - **Interfaces/Mocks**:
> - Add `IsWorkflowExecutionStatusPaused` to `MutableState` interface
and update mocks.
> - **Tests**:
> - New unit tests for paused behavior in handlers, NDC, task refresher,
and transfer queue; functional tests for pause/unpause workflow
behavior.
> 
> <sup>Written by [Cursor
Bugbot]() for commit
17c11c687a0d620cc5a77e6ce9369622feb9214b. This will update automatically
on new commits. Configure
[here]().</sup>
2025-12-15 10:45:31 -08:00

535 lines
19 KiB
Go

package tests
import (
"context"
"strings"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
querypb "go.temporal.io/api/query/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
sdkclient "go.temporal.io/sdk/client"
"go.temporal.io/sdk/workflow"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/tests/testcore"
)
type PauseWorkflowExecutionSuite struct {
testcore.FunctionalTestBase
testEndSignal string
pauseIdentity string
pauseReason string
workflowFn func(ctx workflow.Context) (string, error)
childWorkflowFn func(ctx workflow.Context) (string, error)
activityFn func(ctx context.Context) (string, error)
activityCompletedCh chan struct{}
activityCompletedOnce sync.Once
}
func TestPauseWorkflowExecutionSuite(t *testing.T) {
s := new(PauseWorkflowExecutionSuite)
suite.Run(t, s)
}
func (s *PauseWorkflowExecutionSuite) SetupTest() {
s.FunctionalTestBase.SetupTest()
s.OverrideDynamicConfig(dynamicconfig.WorkflowPauseEnabled, true)
s.testEndSignal = "test-end"
s.pauseIdentity = "functional-test"
s.pauseReason = "pausing workflow for acceptance test"
s.activityCompletedCh = make(chan struct{}, 1)
s.activityCompletedOnce = sync.Once{}
s.workflowFn = func(ctx workflow.Context) (string, error) {
s.T().Log("workflow started")
ao := workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Second,
ScheduleToCloseTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, ao)
var activityResult string
s.T().Log("executing activity")
if err := workflow.ExecuteActivity(ctx, s.activityFn).Get(ctx, &activityResult); err != nil {
return "", err
}
var childResult string
s.T().Log("executing child workflow")
if err := workflow.ExecuteChildWorkflow(ctx, s.childWorkflowFn).Get(ctx, &childResult); err != nil {
return "", err
}
s.T().Log("waiting to receive signal to complete the workflow")
signalCh := workflow.GetSignalChannel(ctx, s.testEndSignal)
var signalPayload string
signalCh.Receive(ctx, &signalPayload)
s.T().Log("signal received to complete the workflow")
return signalPayload + activityResult + childResult, nil
}
s.childWorkflowFn = func(ctx workflow.Context) (string, error) {
return "child-workflow", nil
}
s.activityFn = func(ctx context.Context) (string, error) {
s.T().Log("activity started")
s.activityCompletedOnce.Do(func() {
// blocks until the test case unblocks the activity.
<-s.activityCompletedCh
})
s.T().Log("activity completed")
return "activity", nil
}
s.Worker().RegisterWorkflow(s.workflowFn)
s.Worker().RegisterWorkflow(s.childWorkflowFn)
s.Worker().RegisterActivity(s.activityFn)
}
// TestPauseUnpauseWorkflowExecution tests that the pause and unpause workflow execution APIs work as expected.
// Test sequence:
// 1. Start the workflow.
// 2. Pause the workflow. Assert that the workflow is paused.
// 3. Send signal to the workflow. Assert that the workflow is still paused.
// 4. Unpause the workflow. Assert that the workflow is now running.
// 5. Unblock the activity to complete the workflow.
// 6. Assert that the workflow is completed.
func (s *PauseWorkflowExecutionSuite) TestPauseUnpauseWorkflowExecution() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowOptions := sdkclient.StartWorkflowOptions{
ID: testcore.RandomizeStr("pause-wf-" + s.T().Name()),
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, s.workflowFn)
s.NoError(err)
workflowID := workflowRun.GetID()
runID := workflowRun.GetRunID()
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus())
}, 5*time.Second, 100*time.Millisecond)
pauseRequest := &workflowservice.PauseWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: workflowID,
RunId: runID,
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
pauseResp, err := s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.NoError(err)
s.NotNil(pauseResp)
// ensure that the workflow is paused
s.EventuallyWithT(func(t *assert.CollectT) {
s.assertWorkflowIsPaused(ctx, t, workflowID, runID)
}, 5*time.Second, 200*time.Millisecond)
// Send signal to the workflow to complete the workflow. Since the workflow is paused, it should stay paused.
err = s.SdkClient().SignalWorkflow(ctx, workflowID, runID, s.testEndSignal, "signal to complete the workflow")
s.NoError(err)
s.EventuallyWithT(func(t *assert.CollectT) {
s.assertWorkflowIsPaused(ctx, t, workflowID, runID)
}, 5*time.Second, 200*time.Millisecond)
// Unpause the workflow.
unpauseRequest := &workflowservice.UnpauseWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: workflowID,
RunId: runID,
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
unpauseResp, err := s.FrontendClient().UnpauseWorkflowExecution(ctx, unpauseRequest)
s.NoError(err)
s.NotNil(unpauseResp)
// Workflow status should be running after unpausing. The workflow won't complete until the activity is unblocked.
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus(), "workflow is not running. Status: %s", info.GetStatus())
}, 5*time.Second, 200*time.Millisecond)
// Unblock the activity to complete the workflow.
s.activityCompletedCh <- struct{}{}
// assert that the workflow completes now.
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, info.GetStatus(), "workflow is not completed. Status: %s", info.GetStatus())
}, 10*time.Second, 200*time.Millisecond)
}
func (s *PauseWorkflowExecutionSuite) TestQueryWorkflowWhenPaused() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowOptions := sdkclient.StartWorkflowOptions{
ID: testcore.RandomizeStr("pause-wf-" + s.T().Name()),
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, s.workflowFn)
s.NoError(err)
workflowID := workflowRun.GetID()
runID := workflowRun.GetRunID()
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus())
}, 5*time.Second, 100*time.Millisecond)
// Pause the workflow.
pauseRequest := &workflowservice.PauseWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: workflowID,
RunId: runID,
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
pauseResp, err := s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.NoError(err)
s.NotNil(pauseResp)
// Wait until paused.
s.EventuallyWithT(func(t *assert.CollectT) {
s.assertWorkflowIsPaused(ctx, t, workflowID, runID)
}, 5*time.Second, 200*time.Millisecond)
// Issue a query to the paused workflow. It should return QueryRejected with WORKFLOW_EXECUTION_STATUS_PAUSED status.
queryReq := &workflowservice.QueryWorkflowRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: runID,
},
Query: &querypb.WorkflowQuery{
QueryType: "__stack_trace",
},
}
queryResp, err := s.FrontendClient().QueryWorkflow(ctx, queryReq)
s.NoError(err)
s.NotNil(queryResp)
s.NotNil(queryResp.GetQueryRejected())
s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_PAUSED, queryResp.GetQueryRejected().GetStatus())
// Unpause the workflow.
unpauseRequest := &workflowservice.UnpauseWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: workflowID,
RunId: runID,
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
unpauseResp, err := s.FrontendClient().UnpauseWorkflowExecution(ctx, unpauseRequest)
s.NoError(err)
s.NotNil(unpauseResp)
// Unblock the activity and send the signal to complete the workflow.
s.activityCompletedCh <- struct{}{}
err = s.SdkClient().SignalWorkflow(ctx, workflowID, runID, s.testEndSignal, "test end signal")
s.NoError(err)
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, info.GetStatus())
}, 5*time.Second, 200*time.Millisecond)
}
// TestPauseWorkflowExecutionRequestValidation tests that pause workflow execution request validation. We don't really need a valid workflow to test this.
// - fails when the identity is too long.
// - fails when the reason is too long.
// - fails when the request id is too long.
// - fails when the dynamic config is disabled.
func (s *PauseWorkflowExecutionSuite) TestPauseWorkflowExecutionRequestValidation() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
namespaceName := s.Namespace().String()
// fails when the identity is too long.
pauseRequest := &workflowservice.PauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: strings.Repeat("x", 2000),
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
resp, err := s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.Error(err)
s.Nil(resp)
var invalidArgumentErr *serviceerror.InvalidArgument
s.ErrorAs(err, &invalidArgumentErr)
s.NotNil(invalidArgumentErr)
s.Contains(invalidArgumentErr.Error(), "identity is too long.")
// fails when the reason is too long.
pauseRequest = &workflowservice.PauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: s.pauseIdentity,
Reason: strings.Repeat("x", 2000),
RequestId: uuid.NewString(),
}
resp, err = s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.Error(err)
s.Nil(resp)
s.ErrorAs(err, &invalidArgumentErr)
s.NotNil(invalidArgumentErr)
s.Contains(invalidArgumentErr.Error(), "reason is too long.")
// fails when the request id is too long.
pauseRequest = &workflowservice.PauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: strings.Repeat("x", 2000),
}
resp, err = s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.Error(err)
s.Nil(resp)
s.ErrorAs(err, &invalidArgumentErr)
s.NotNil(invalidArgumentErr)
s.Contains(invalidArgumentErr.Error(), "request id is too long.")
// fails when the dynamic config is disabled.
s.OverrideDynamicConfig(dynamicconfig.WorkflowPauseEnabled, false)
pauseRequest = &workflowservice.PauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
resp, err = s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.Error(err)
s.Nil(resp)
var unimplementedErr *serviceerror.Unimplemented
s.ErrorAs(err, &unimplementedErr)
s.NotNil(unimplementedErr)
s.Contains(unimplementedErr.Error(), namespaceName)
}
// TestUnpauseWorkflowExecutionRequestValidation tests unpause workflow execution request validation. We don't need a valid workflow.
// - fails when the identity is too long.
// - fails when the reason is too long.
// - fails when the request id is too long.
func (s *PauseWorkflowExecutionSuite) TestUnpauseWorkflowExecutionRequestValidation() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
namespaceName := s.Namespace().String()
// fails when the identity is too long.
unpauseRequest := &workflowservice.UnpauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: strings.Repeat("x", 2000),
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
resp, err := s.FrontendClient().UnpauseWorkflowExecution(ctx, unpauseRequest)
s.Error(err)
s.Nil(resp)
var invalidArgumentErr *serviceerror.InvalidArgument
s.ErrorAs(err, &invalidArgumentErr)
s.NotNil(invalidArgumentErr)
s.Contains(invalidArgumentErr.Error(), "identity is too long.")
// fails when the reason is too long.
unpauseRequest = &workflowservice.UnpauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: s.pauseIdentity,
Reason: strings.Repeat("x", 2000),
RequestId: uuid.NewString(),
}
resp, err = s.FrontendClient().UnpauseWorkflowExecution(ctx, unpauseRequest)
s.Error(err)
s.Nil(resp)
s.ErrorAs(err, &invalidArgumentErr)
s.NotNil(invalidArgumentErr)
s.Contains(invalidArgumentErr.Error(), "reason is too long.")
// fails when the request id is too long.
unpauseRequest = &workflowservice.UnpauseWorkflowExecutionRequest{
Namespace: namespaceName,
WorkflowId: "test-workflow-id",
RunId: uuid.NewString(),
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: strings.Repeat("x", 2000),
}
resp, err = s.FrontendClient().UnpauseWorkflowExecution(ctx, unpauseRequest)
s.Error(err)
s.Nil(resp)
s.ErrorAs(err, &invalidArgumentErr)
s.NotNil(invalidArgumentErr)
s.Contains(invalidArgumentErr.Error(), "request id is too long.")
}
func (s *PauseWorkflowExecutionSuite) TestPauseWorkflowExecutionAlreadyPaused() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowOptions := sdkclient.StartWorkflowOptions{
ID: testcore.RandomizeStr("pause-wf-" + s.T().Name()),
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, s.workflowFn)
s.NoError(err)
workflowID := workflowRun.GetID()
runID := workflowRun.GetRunID()
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus())
}, 5*time.Second, 100*time.Millisecond)
// 1st pause request should succeed.
pauseRequest := &workflowservice.PauseWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: workflowID,
RunId: runID,
Identity: s.pauseIdentity,
Reason: s.pauseReason,
RequestId: uuid.NewString(),
}
pauseResp, err := s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.NoError(err)
s.NotNil(pauseResp)
// Wait until paused.
s.EventuallyWithT(func(t *assert.CollectT) {
s.assertWorkflowIsPaused(ctx, t, workflowID, runID)
}, 5*time.Second, 200*time.Millisecond)
// 2nd pause request should fail with failed precondition error.
pauseRequest.RequestId = uuid.NewString()
pauseResp, err = s.FrontendClient().PauseWorkflowExecution(ctx, pauseRequest)
s.Error(err)
s.Nil(pauseResp)
var failedPreconditionErr *serviceerror.FailedPrecondition
s.ErrorAs(err, &failedPreconditionErr)
s.NotNil(failedPreconditionErr)
s.Contains(failedPreconditionErr.Error(), "workflow is already paused.")
unpauseRequest := &workflowservice.UnpauseWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: workflowID,
RunId: runID,
Identity: s.pauseIdentity,
Reason: "cleanup after paused workflow test",
RequestId: uuid.NewString(),
}
unpauseResp, err := s.FrontendClient().UnpauseWorkflowExecution(ctx, unpauseRequest)
s.NoError(err)
s.NotNil(unpauseResp)
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus())
require.Nil(t, desc.GetWorkflowExtendedInfo().GetPauseInfo())
}, 5*time.Second, 200*time.Millisecond)
// Unblock the activity and send the signal to complete the workflow.
s.activityCompletedCh <- struct{}{}
err = s.SdkClient().SignalWorkflow(ctx, workflowID, runID, s.testEndSignal, "test end signal")
s.NoError(err)
s.EventuallyWithT(func(t *assert.CollectT) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, info.GetStatus())
}, 5*time.Second, 200*time.Millisecond)
}
// assertWorkflowIsPaused is a helper method which asserts that,
// - the workflow status is paused.
// - the workflow has the correct pause info.
// - there is no workflow task scheduled event inbetween pause and unpause events.
func (s *PauseWorkflowExecutionSuite) assertWorkflowIsPaused(ctx context.Context, t *assert.CollectT, workflowID string, runID string) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID)
require.NoError(t, err)
info := desc.GetWorkflowExecutionInfo()
require.NotNil(t, info)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_PAUSED, info.GetStatus(), "workflow is not paused. Status: %s", info.GetStatus())
if pauseInfo := desc.GetWorkflowExtendedInfo().GetPauseInfo(); pauseInfo != nil {
require.Equal(t, s.pauseIdentity, pauseInfo.GetIdentity(), "pause identity is not correct")
require.Equal(t, s.pauseReason, pauseInfo.GetReason(), "pause reason is not correct")
}
// Also assert that there is no workflow task scheduled event after the pause event.
hist := s.SdkClient().GetWorkflowHistory(ctx, workflowID, runID, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
isPaused := false
for hist.HasNext() {
event, err := hist.Next()
require.NoError(t, err)
if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_PAUSED {
isPaused = true
continue
}
if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UNPAUSED {
isPaused = false
continue
}
if isPaused {
// These are all events after the pause event. None of these should be workflow task scheduled events.
require.NotEqual(t, enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, event.EventType)
}
}
}