Files
temporal/tests/activity_api_reset_test.go
Stephan Behnke 3ba31f2ac0 Add context-aware channel test helpers (#11700)
Adds generic `await.Rcv` and `await.Snd` helpers that bound blocking
channel operations by the test context and report closure or
cancellation clearly.

Replaces both previous helpers and existing unsafe channel rcv/snd
across tests.
2026-08-24 19:30:41 -07:00

826 lines
30 KiB
Go

package tests
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/activity"
sdkclient "go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/testing/await"
"go.temporal.io/server/common/testing/testvars"
"go.temporal.io/server/common/util"
"go.temporal.io/server/tests/testcore"
)
type ActivityApiResetClientTestSuite struct {
testcore.FunctionalTestBase //nolint:forbidigo
tv *testvars.TestVars
initialRetryInterval time.Duration
scheduleToCloseTimeout time.Duration
startToCloseTimeout time.Duration
activityRetryPolicy *temporal.RetryPolicy
// apiName selects which reset API variant to exercise ("legacy-api" or "execution-api").
// Set before suite.Run; used by SetupTest to initialise resetFn.
apiName string
// resetFn is the adapter for the API under test, initialised in SetupTest.
resetFn func(ctx context.Context, wfID, actID string, resetHeartbeat, keepPaused bool) error
}
// TestActivityApiResetClientTestSuite runs the suite twice: once with the legacy
// ResetActivity API and once with the newer ResetActivityExecution API.
func TestActivityApiResetClientTestSuite(t *testing.T) {
for _, apiName := range []string{"legacy-api", "execution-api"} {
t.Run(apiName, func(t *testing.T) {
s := new(ActivityApiResetClientTestSuite)
s.apiName = apiName
suite.Run(t, s)
})
}
}
func (s *ActivityApiResetClientTestSuite) SetupTest() {
s.FunctionalTestBase.SetupTest() //nolint:forbidigo
s.tv = testvars.New(s.T()).WithTaskQueue(s.TaskQueue()).WithNamespaceName(s.Namespace())
s.initialRetryInterval = 1 * time.Second
s.scheduleToCloseTimeout = 30 * time.Minute
s.startToCloseTimeout = 15 * time.Minute
s.activityRetryPolicy = &temporal.RetryPolicy{
InitialInterval: s.initialRetryInterval,
BackoffCoefficient: 1,
}
if s.apiName == "execution-api" {
s.resetFn = func(ctx context.Context, wfID, actID string, resetHeartbeat, keepPaused bool) error {
_, err := s.FrontendClient().ResetActivityExecution(ctx, &workflowservice.ResetActivityExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowId: wfID,
ActivityId: actID,
ResetHeartbeat: resetHeartbeat,
KeepPaused: keepPaused,
})
return err
}
} else {
s.resetFn = func(ctx context.Context, wfID, actID string, resetHeartbeat, keepPaused bool) error {
_, err := s.FrontendClient().ResetActivity(ctx, &workflowservice.ResetActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{WorkflowId: wfID},
Activity: &workflowservice.ResetActivityRequest_Id{Id: actID},
ResetHeartbeat: resetHeartbeat,
KeepPaused: keepPaused,
})
return err
}
}
}
func (s *ActivityApiResetClientTestSuite) makeWorkflowFunc(activityFunction ActivityFunctions) WorkflowFunction {
return func(ctx workflow.Context) error {
var ret string
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ActivityID: "activity-id",
DisableEagerExecution: true,
StartToCloseTimeout: s.startToCloseTimeout,
ScheduleToCloseTimeout: s.scheduleToCloseTimeout,
RetryPolicy: s.activityRetryPolicy,
}), activityFunction).Get(ctx, &ret)
return err
}
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_AfterRetry() {
// activity reset is called after multiple attempts,
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var activityWasReset atomic.Bool
activityCompleteCh := make(chan struct{})
var startedActivityCount atomic.Int32
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
if activityWasReset.Load() == false {
activityErr := errors.New("bad-luck-please-retry")
return "", activityErr
}
await.Rcv(s.T(), activityCompleteCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
wfId := testcore.RandomizeStr("wfid-" + s.T().Name())
workflowOptions := sdkclient.StartWorkflowOptions{
ID: wfId,
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn)
s.NoError(err)
// wait for activity to start/fail few times
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Greater(t, startedActivityCount.Load(), int32(1))
}, 5*time.Second, 200*time.Millisecond)
s.NoError(s.resetFn(ctx, workflowRun.GetID(), "activity-id", false, false))
activityWasReset.Store(true)
// wait for activity to be running
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State)
// also verify that the number of attempts was reset
require.Equal(t, int32(1), description.PendingActivities[0].Attempt)
}, 5*time.Second, 100*time.Millisecond)
// let activity finish
activityCompleteCh <- struct{}{}
// wait for workflow to complete
var out string
err = workflowRun.Get(ctx, &out)
s.NoError(err)
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_WhileRunning() {
// activity reset is called while activity is running
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
activityCompleteCh := make(chan struct{})
var startedActivityCount atomic.Int32
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
await.Rcv(s.T(), activityCompleteCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
workflowOptions := sdkclient.StartWorkflowOptions{
ID: s.tv.WorkflowID(),
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn)
s.NoError(err)
// wait for activity to start
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State)
}, 5*time.Second, 200*time.Millisecond)
s.NoError(s.resetFn(ctx, workflowRun.GetID(), "activity-id", false, false))
// wait a bit
util.InterruptibleSleep(ctx, 1*time.Second)
// check if workflow and activity are still running
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State)
// also verify that the number of attempts was reset
require.Equal(t, int32(1), description.PendingActivities[0].Attempt)
}, 5*time.Second, 100*time.Millisecond)
// let activity finish
activityCompleteCh <- struct{}{}
// wait for workflow to complete
var out string
err = workflowRun.Get(ctx, &out)
s.NoError(err)
// make sure that only a single instance of the activity was running
s.Equal(int32(1), startedActivityCount.Load())
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_UnpausesRunningActivity() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
activityCompleteCh := make(chan struct{})
var startedActivityCount atomic.Int32
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
await.Rcv(s.T(), activityCompleteCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
ID: s.tv.WorkflowID(),
TaskQueue: s.TaskQueue(),
}, workflowFn)
s.Require().NoError(err)
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State)
}, 5*time.Second, 200*time.Millisecond)
_, err = s.FrontendClient().PauseActivity(ctx, &workflowservice.PauseActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{WorkflowId: workflowRun.GetID()},
Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"},
})
s.Require().NoError(err)
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED, description.PendingActivities[0].State)
require.True(t, description.PendingActivities[0].Paused)
require.NotNil(t, description.PendingActivities[0].PauseInfo)
}, 5*time.Second, 200*time.Millisecond)
s.Require().NoError(s.resetFn(ctx, workflowRun.GetID(), "activity-id", false, false))
// keepPaused=false must clear both the paused flag and its associated metadata.
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State)
require.False(t, description.PendingActivities[0].Paused)
require.Nil(t, description.PendingActivities[0].PauseInfo)
require.Equal(t, int32(1), description.PendingActivities[0].Attempt)
}, 5*time.Second, 200*time.Millisecond)
activityCompleteCh <- struct{}{}
s.Require().NoError(workflowRun.Get(ctx, nil))
s.Equal(int32(1), startedActivityCount.Load())
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_TimesOutOnUnpause() {
s.startToCloseTimeout = time.Second
s.activityRetryPolicy = &temporal.RetryPolicy{MaximumAttempts: 1}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
activityCompleteCh := make(chan struct{})
defer close(activityCompleteCh)
activityFunction := func() (string, error) {
<-activityCompleteCh
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
ID: s.tv.WorkflowID(),
TaskQueue: s.TaskQueue(),
}, workflowFn)
s.Require().NoError(err)
var activityStartedAt time.Time
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
pendingActivity := description.PendingActivities[0]
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, pendingActivity.State)
require.NotNil(t, pendingActivity.LastStartedTime)
activityStartedAt = pendingActivity.LastStartedTime.AsTime()
}, 5*time.Second, 200*time.Millisecond)
_, err = s.FrontendClient().PauseActivity(ctx, &workflowservice.PauseActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{WorkflowId: workflowRun.GetID()},
Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"},
})
s.Require().NoError(err)
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED, description.PendingActivities[0].State)
require.True(t, description.PendingActivities[0].Paused)
}, 5*time.Second, 200*time.Millisecond)
// Allow the original timeout task to be processed while the activity is paused.
originalDeadline := activityStartedAt.Add(s.startToCloseTimeout)
await.RequireTrue(s.T(), func() bool {
return time.Now().After(originalDeadline.Add(2 * time.Second))
}, 5*time.Second, 100*time.Millisecond)
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
s.Require().NoError(err)
s.Require().Len(description.GetPendingActivities(), 1)
s.True(description.PendingActivities[0].Paused)
s.Require().NoError(s.resetFn(ctx, workflowRun.GetID(), "activity-id", false, false))
workflowResultCtx, workflowResultCancel := context.WithTimeout(ctx, 10*time.Second)
defer workflowResultCancel()
err = workflowRun.Get(workflowResultCtx, nil)
s.Require().Error(err)
var activityErr *temporal.ActivityError
s.Require().ErrorAs(err, &activityErr)
timeoutErr, ok := activityErr.Unwrap().(*temporal.TimeoutError)
s.Require().True(ok)
s.Equal(enumspb.TIMEOUT_TYPE_START_TO_CLOSE, timeoutErr.TimeoutType())
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_InRetry() {
// reset is called while activity is in retry
s.initialRetryInterval = 1 * time.Minute
s.activityRetryPolicy = &temporal.RetryPolicy{
InitialInterval: s.initialRetryInterval,
BackoffCoefficient: 1,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var startedActivityCount atomic.Int32
activityCompleteCh := make(chan struct{})
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
if startedActivityCount.Load() == 1 {
activityErr := errors.New("bad-luck-please-retry")
return "", activityErr
}
await.Rcv(s.T(), activityCompleteCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
wfId := testcore.RandomizeStr("wf_id-" + s.T().Name())
workflowOptions := sdkclient.StartWorkflowOptions{
ID: wfId,
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn)
s.NoError(err)
// wait for activity to start, fail and wait for retry
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.PendingActivities, 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_SCHEDULED, description.PendingActivities[0].State)
require.Equal(t, int32(1), startedActivityCount.Load())
}, 5*time.Second, 200*time.Millisecond)
s.NoError(s.resetFn(ctx, workflowRun.GetID(), "activity-id", false, false))
// wait for activity to start. Wait time is shorter than original retry interval
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State)
require.Equal(t, int32(2), startedActivityCount.Load())
// also verify that the number of attempts was reset
require.Equal(t, int32(1), description.PendingActivities[0].Attempt)
}, 2*time.Second, 200*time.Millisecond)
// let previous activity complete
activityCompleteCh <- struct{}{}
// wait for workflow to complete
var out string
err = workflowRun.Get(ctx, &out)
s.NoError(err)
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_KeepPaused() {
// reset is called while activity is in retry
s.initialRetryInterval = 1 * time.Minute
s.activityRetryPolicy = &temporal.RetryPolicy{
InitialInterval: s.initialRetryInterval,
BackoffCoefficient: 1,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var startedActivityCount atomic.Int32
var activityWasReset atomic.Bool
activityCompleteCh := make(chan struct{})
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
if !activityWasReset.Load() {
activityErr := errors.New("bad-luck-please-retry")
return "", activityErr
}
await.Rcv(s.T(), activityCompleteCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
wfId := testcore.RandomizeStr("wf_id-" + s.T().Name())
workflowOptions := sdkclient.StartWorkflowOptions{
ID: wfId,
TaskQueue: s.TaskQueue(),
}
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn)
s.NoError(err)
// wait for activity to start, fail few times and wait for retry
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.PendingActivities, 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_SCHEDULED, description.PendingActivities[0].State)
require.Greater(t, description.PendingActivities[0].Attempt, int32(1))
}, 5*time.Second, 200*time.Millisecond)
// pause the activity
pauseRequest := &workflowservice.PauseActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{
WorkflowId: workflowRun.GetID(),
},
Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"},
}
pauseResp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest)
s.NoError(err)
s.NotNil(pauseResp)
// verify that activity is paused
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.NotNil(t, description)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED, description.PendingActivities[0].State)
// also verify that the number of attempts was not reset
require.Greater(t, description.PendingActivities[0].Attempt, int32(1))
require.True(t, description.PendingActivities[0].Paused)
}, 5*time.Second, 100*time.Millisecond)
// reset the activity, while keeping it paused
s.NoError(s.resetFn(ctx, workflowRun.GetID(), "activity-id", false, true))
// verify that activity is still paused, and reset
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.NotNil(t, description)
require.Len(t, description.GetPendingActivities(), 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED, description.PendingActivities[0].State)
// also verify that the number of attempts was reset
require.Equal(t, int32(1), description.PendingActivities[0].Attempt)
}, 2*time.Second, 200*time.Millisecond)
// let activity stop failing
activityWasReset.Store(true)
// unpause the activity
unpauseRequest := &workflowservice.UnpauseActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{
WorkflowId: workflowRun.GetID(),
},
Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"},
}
unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest)
s.NoError(err)
s.NotNil(unpauseResp)
// let activity complete
activityCompleteCh <- struct{}{}
// wait for workflow to complete
var out string
err = workflowRun.Get(ctx, &out)
s.NoError(err)
}
func requirePayload(t require.TestingT, expected string, pls *commonpb.Payloads) {
require.NotNil(t, pls)
require.NotNil(t, pls.Payloads)
require.Len(t, pls.Payloads, 1)
var actual string
err := payloads.Decode(pls, &actual)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
// TestActivityReset_HeartbeatDetails covers the default: a reset rewinds the attempt count but
// keeps the heartbeat checkpoint.
func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() {
s.runResetHeartbeatDetails(false, true)
}
// TestActivityReset_HeartbeatDetailsWithResetHeartbeatFlag covers the opt-in discard.
func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetailsWithResetHeartbeatFlag() {
s.runResetHeartbeatDetails(true, false)
}
// runResetHeartbeatDetails runs an activity that heartbeats "first", resets it mid-attempt, then
// lets that attempt fail so the reset lands on the retry, and checks whether the checkpoint
// survived. The retried attempt heartbeats "second" to show heartbeating still works afterwards.
func (s *ActivityApiResetClientTestSuite) runResetHeartbeatDetails(resetHeartbeat, expectPreserved bool) {
activityCompleteCh := make(chan struct{})
var activityIteration atomic.Int32
var activityShouldBreak atomic.Bool
var activityShouldFinish atomic.Bool
activityFn := func(ctx context.Context) (string, error) {
if activityIteration.Load() == 0 {
for activityShouldBreak.Load() == false {
activity.RecordHeartbeat(ctx, "first")
time.Sleep(time.Second) //nolint:forbidigo
}
return "", errors.New("bad-luck-please-retry")
}
// not the first iteration
await.Rcv(s.T(), activityCompleteCh)
for activityShouldFinish.Load() == false {
activity.RecordHeartbeat(ctx, "second")
time.Sleep(time.Second) //nolint:forbidigo
}
return "Done", nil
}
activityId := "heartbeat_retry"
workflowFn := func(ctx workflow.Context) (string, error) {
var ret string
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ActivityID: activityId,
DisableEagerExecution: true,
StartToCloseTimeout: s.startToCloseTimeout,
ScheduleToCloseTimeout: s.scheduleToCloseTimeout,
RetryPolicy: s.activityRetryPolicy,
}), activityFn).Get(ctx, &ret)
return ret, err
}
s.SdkWorker().RegisterActivity(activityFn)
s.SdkWorker().RegisterWorkflow(workflowFn)
wfID := testcore.RandomizeStr("wfid-" + s.T().Name())
workflowOptions := sdkclient.StartWorkflowOptions{
ID: wfID,
TaskQueue: s.TaskQueue(),
WorkflowRunTimeout: 20 * time.Second,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn)
s.NoError(err)
s.NotNil(workflowRun)
runId := workflowRun.GetRunID()
s.NotEmpty(runId)
// make sure activity is running and sending heartbeats
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.PendingActivities, 1)
requirePayload(t, "first", description.PendingActivities[0].GetHeartbeatDetails())
require.Equal(t, int32(0), activityIteration.Load())
}, 5*time.Second, 500*time.Millisecond)
s.NoError(s.resetFn(ctx, workflowRun.GetID(), activityId, resetHeartbeat, false))
activityIteration.Store(1)
activityShouldBreak.Store(true)
// wait for activity to fail and retried
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, description.PendingActivities, 1)
ap := description.PendingActivities[0]
require.Equal(t, int32(2), ap.Attempt)
if expectPreserved {
requirePayload(t, "first", ap.GetHeartbeatDetails())
} else {
require.Nil(t, ap.HeartbeatDetails)
}
require.Equal(t, int32(1), activityIteration.Load())
}, 5*time.Second, 500*time.Millisecond)
// let activity start producing heartbeats
activityCompleteCh <- struct{}{}
// make sure activity is running and sending heartbeats
await.Require(ctx, s.T(), func(t *await.T) {
description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Equal(t, int32(1), activityIteration.Load())
require.Len(t, description.PendingActivities, 1)
requirePayload(t, "second", description.PendingActivities[0].GetHeartbeatDetails())
}, 5*time.Second, 500*time.Millisecond)
// let activity finish
activityShouldFinish.Store(true)
// wait for workflow to finish
var out string
err = workflowRun.Get(ctx, &out)
s.NoError(err)
s.NotEmpty(out)
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_WhilePaused() {
// Reset is called while the activity is in PAUSED state (SCHEDULED→PAUSED via TransitionPaused).
// The activity should remain PAUSED with attempt count reset to 1. After unpause it should complete.
s.initialRetryInterval = 1 * time.Minute
s.activityRetryPolicy = &temporal.RetryPolicy{
InitialInterval: s.initialRetryInterval,
BackoffCoefficient: 1,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var startedActivityCount atomic.Int32
var activityWasReset atomic.Bool
activityCompleteCh := make(chan struct{})
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
if !activityWasReset.Load() {
return "", errors.New("bad-luck-please-retry")
}
await.Rcv(s.T(), activityCompleteCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
wfID := testcore.RandomizeStr("wf_id-" + s.T().Name())
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
ID: wfID,
TaskQueue: s.TaskQueue(),
}, workflowFn)
s.NoError(err)
// wait for activity to fail and enter retry backoff (SCHEDULED state waiting for retry)
await.Require(ctx, s.T(), func(t *await.T) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, desc.PendingActivities, 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_SCHEDULED, desc.PendingActivities[0].State)
require.Greater(t, desc.PendingActivities[0].Attempt, int32(1))
}, 5*time.Second, 200*time.Millisecond)
// pause the activity (transitions SCHEDULED→PAUSED)
_, err = s.FrontendClient().PauseActivity(ctx, &workflowservice.PauseActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{WorkflowId: wfID},
Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"},
})
s.NoError(err)
// wait for PAUSED state
await.Require(ctx, s.T(), func(t *await.T) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, desc.PendingActivities, 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED, desc.PendingActivities[0].State)
require.Greater(t, desc.PendingActivities[0].Attempt, int32(1))
}, 5*time.Second, 100*time.Millisecond)
// reset while paused — activity should stay PAUSED, but attempt resets to 1
s.NoError(s.resetFn(ctx, wfID, "activity-id", false, true))
await.Require(ctx, s.T(), func(t *await.T) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, desc.PendingActivities, 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED, desc.PendingActivities[0].State)
require.Equal(t, int32(1), desc.PendingActivities[0].Attempt)
}, 5*time.Second, 100*time.Millisecond)
activityWasReset.Store(true)
// unpause — activity should run and complete
_, err = s.FrontendClient().UnpauseActivity(ctx, &workflowservice.UnpauseActivityRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{WorkflowId: wfID},
Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"},
})
s.NoError(err)
activityCompleteCh <- struct{}{}
s.NoError(workflowRun.Get(ctx, nil))
}
func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_TerminateWhileDeferredReset() {
// Reset is called while activity is STARTED (sets ActivityReset=true as a deferred flag).
// The workflow is then terminated before the activity retries. Verifies the activity
// and workflow terminate cleanly without the deferred reset flag causing issues.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
activityBlockCh := make(chan struct{})
var startedActivityCount atomic.Int32
activityFunction := func() (string, error) {
startedActivityCount.Add(1)
await.Rcv(s.T(), activityBlockCh)
return "done!", nil
}
workflowFn := s.makeWorkflowFunc(activityFunction)
s.SdkWorker().RegisterWorkflow(workflowFn)
s.SdkWorker().RegisterActivity(activityFunction)
wfID := testcore.RandomizeStr("wf_id-" + s.T().Name())
workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
ID: wfID,
TaskQueue: s.TaskQueue(),
}, workflowFn)
s.NoError(err)
// wait for activity to start
await.Require(ctx, s.T(), func(t *await.T) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Len(t, desc.PendingActivities, 1)
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, desc.PendingActivities[0].State)
}, 5*time.Second, 200*time.Millisecond)
// reset while running — sets ActivityReset=true as deferred flag
s.NoError(s.resetFn(ctx, wfID, "activity-id", false, false))
// terminate the workflow before the activity retries
err = s.SdkClient().TerminateWorkflow(ctx, wfID, workflowRun.GetRunID(), "test termination")
s.NoError(err)
// unblock the activity worker so it can respond
close(activityBlockCh)
// verify the workflow is terminated
await.Require(ctx, s.T(), func(t *await.T) {
desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID())
require.NoError(t, err)
require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, desc.GetWorkflowExecutionInfo().GetStatus())
}, 10*time.Second, 200*time.Millisecond)
}