Skip to content

fix(activity): reject updates and unpause during pending reset - #11360

Draft
spkane31 wants to merge 5 commits into
mainfrom
spk/failed-preconditions
Draft

fix(activity): reject updates and unpause during pending reset#11360
spkane31 wants to merge 5 commits into
mainfrom
spk/failed-preconditions

Conversation

@spkane31

Copy link
Copy Markdown
Contributor

What changed?

  • Reject UpdateActivityExecutionOptions while a reset is pending.
  • Reject UnpauseActivityExecuiont during a deferred keep-paused reset.
  • Use transition applicability checks for valid unpause transitions.

Why?

SAA crew (me, @fretz12, @dandavison) agree that these niche state transitions are better off as a FailedPrecondition and can be allowed in the future or more specific errors can be given.

How did you test it?

  • covered by existing tests
  • added/updated functional test(s)

Potential risks

This intentionally changes previously allowed option updates during RESET_REQUESTED to return FailedPrecondition.

@spkane31
spkane31 requested review from a team as code owners July 30, 2026 22:17
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @spkane31's task in 4m 57s —— View job


Review complete

  • Read review skill (.claude/skills/review/SKILL.md.github/copilot-instructions.md)
  • Read PR diff against origin/main
  • Traced UpdateActivityExecutionOptions / handleUnpauseRequested state handling, transitions, isPaused, runState, reissueRunningAttemptTimers
  • Checked for affected unit tests (activity_test.go, statemachine_test.go) and the parity model
  • Posted findings as inline comments (3)

The core change looks sound. The switch a.GetStatus()switch { case Transition.Possible(a) } rewrite in handleUnpauseRequested is behavior-preserving: TransitionUnpaused sources are exactly PAUSED and TransitionUnpausedWhilePauseRequested exactly PAUSE_REQUESTED, so the only reachable default is RESET_REQUESTED with ResetKeepPaused — which is the intended new rejection. RESET_REQUESTED without keep-paused still short-circuits as a no-op via !a.isPaused(), unchanged. No unit test in chasm/lib/activity asserts the old behavior, and the parity vocabulary has no unpause event, so nothing there regresses.

Findings are inline: an internal-status leak in the new rejection error (plus a stale doc comment and a coverage gap left by the deleted timer test), a single-read state assertion that the rest of the file wraps in waitForState, and a test comment that documents the decision rather than the behavior.

Not done: I could not run go build/go vet/tests — the sandbox denied the build commands, so all analysis is static.
• Branch: spk/failed-preconditions

Comment thread chasm/lib/activity/activity.go Outdated
case activitypb.ACTIVITY_EXECUTION_STATUS_CANCELED,
activitypb.ACTIVITY_EXECUTION_STATUS_COMPLETED,
activitypb.ACTIVITY_EXECUTION_STATUS_FAILED,
activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folding RESET_REQUESTED into the terminal-state case makes the resulting error leak an internal status. RESET_REQUESTED has no public counterpart — runState() deliberately reports it as STARTED or PAUSE_REQUESTED because "the public PendingActivityState enum does not have a RESET_REQUESTED variant" (activity.go:1667-1670). So a caller who just saw STARTED from DescribeActivityExecution gets Cannot update options for activity in state ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED, naming a state they can't observe. It also reads as permanent, when this rejection is transient — the update succeeds again once the worker yields.

A separate check ahead of the terminal switch keeps the two rejection reasons distinct:

	if a.Status == activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED {
		return nil, serviceerror.NewFailedPrecondition("cannot update options while a reset is pending")
	}
	switch a.Status {
	case activitypb.ACTIVITY_EXECUTION_STATUS_CANCELED,
	...

Two follow-ons from this line:

  • reissueRunningAttemptTimers's doc comment (activity.go:1463-1467) still lists RESET_REQUESTED among the statuses it re-emits timers for. UpdateActivityExecutionOptions is its only caller, so that status is now unreachable there and the comment is stale.
  • Deleting UpdateOptionsPreservesTimeoutsWhileResetRequested removes the only coverage of that re-emission path for a running attempt. Worth confirming an equivalent case still exists for STARTED/PAUSE_REQUESTED, otherwise the stamp-bump-invalidates-timers regression it guarded is now untested.

Also worth stating in the PR risks: shortening StartToCloseTimeout was an operator escape hatch for a worker that stops heartbeating while a reset is pending. That's gone; recovery now waits on the pre-existing timeouts or a terminate.

Comment thread tests/activity_standalone_test.go Outdated
Comment on lines +13619 to +13622
// The deferred reset should land the activity in PAUSED, honoring the keep-paused intent
// that survived the rejected unpause (rather than dispatching a new attempt).
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED,
describeActivity(ctx, t, activityID, startResp.GetRunId()).GetInfo().GetRunState())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every sibling assertion of this same transition — deferred keep-paused reset applying when the worker yields — waits rather than reading once: line 12999 (waitForState(... PENDING_ACTIVITY_STATE_PAUSED)) and lines 13537-13546 (await.Require). A bare describeActivity here asserts on the first read after failAttemptRetryably returns, so if that state isn't visible to Describe the instant the respond RPC returns, the subtest fails outright instead of retrying. waitForState is already in scope and used at line 13585 in this same subtest.

Suggested change
// The deferred reset should land the activity in PAUSED, honoring the keep-paused intent
// that survived the rejected unpause (rather than dispatching a new attempt).
require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED,
describeActivity(ctx, t, activityID, startResp.GetRunId()).GetInfo().GetRunState())
// The deferred reset should land the activity in PAUSED, honoring the keep-paused intent
// that survived the rejected unpause.
waitForState(ctx, t, activityID, startResp.GetRunId(), enumspb.PENDING_ACTIVITY_STATE_PAUSED)

Comment thread tests/activity_standalone_test.go Outdated
Comment on lines +13570 to +13572
// reports back. An unpause request that arrives in that window is rejected outright:
// RESET_REQUESTED is a niche state permutation the server refuses rather than resolving
// ambiguously (e.g. by clearing the pending keep-paused intent).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This explains the decision process rather than the test: "niche state permutation", "refuses rather than resolving ambiguously", and the parenthetical counterfactual about what the server could have done. The repo review guidelines call out exactly these (no counterfactuals or authoring-time discussion, no metaphorical/alien phrasing, simple sentence structure). The behavior is what the test needs to state:

Suggested change
// reports back. An unpause request that arrives in that window is rejected outright:
// RESET_REQUESTED is a niche state permutation the server refuses rather than resolving
// ambiguously (e.g. by clearing the pending keep-paused intent).
// reports back. An unpause request that arrives in that window is rejected.

Comment thread chasm/lib/activity/activity.go Outdated
req *activitypb.UpdateActivityExecutionOptionsRequest,
) (*activitypb.UpdateActivityExecutionOptionsResponse, error) {
frontendReq := req.GetFrontendRequest()
if a.Status == activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might have been before my merge, but move this after the request ID check below

Comment thread chasm/lib/activity/activity.go Outdated

if a.Status == activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED {
return nil, serviceerror.NewFailedPrecondition("cannot update options while a reset is pending")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just put this in the switch statement?

}
case activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED:
a.ResetKeepPaused = false
default:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Maybe one day we can think about whether it's possible to change the state machine library to allow expressing conditional destination states. The state machine library in sdk-core does support it FWIW.

// (STARTED / CANCEL_REQUESTED / PAUSE_REQUESTED).
func (a *Activity) reissueRunningAttemptTimers(ctx chasm.MutableContext, attempt *activitypb.ActivityAttemptState) {
if !a.hasAttemptInProgress() {
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classic example of agent comments trying to explain how the entire code base works in a comment. Let's just nuke it. Seems like it was correct to have RESET_REQUESTED in there. But rather than having to think about it, let's just get rid of the comment.

@spkane31
spkane31 enabled auto-merge (squash) July 31, 2026 19:14
@spkane31
spkane31 force-pushed the spk/failed-preconditions branch from 87e320d to e0860f8 Compare July 31, 2026 19:14
@spkane31
spkane31 force-pushed the spk/failed-preconditions branch from f0b8296 to c005ce5 Compare August 3, 2026 15:00
@spkane31
spkane31 requested a review from a team August 3, 2026 15:00
@spkane31
spkane31 disabled auto-merge August 3, 2026 17:43
@spkane31
spkane31 marked this pull request as draft August 3, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants