-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Preserve NextRetryDelay across unrelated activity option updates #10920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5ae4517
284e657
bdd533e
25a3eb6
b15182f
d2abbbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ import ( | |||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||
| "math/rand" | ||||||||||||||||||||||||
| "slices" | ||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| "github.com/nexus-rpc/sdk-go/nexus" | ||||||||||||||||||||||||
|
|
@@ -639,13 +640,14 @@ func (a *Activity) UpdateActivityExecutionOptions( | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| frontendReq := req.GetFrontendRequest() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // start_delay updates are only valid while the activity is still in its delay window. | ||||||||||||||||||||||||
| var hasStartDelayInMask bool | ||||||||||||||||||||||||
| updateFields := map[string]struct{}{} | ||||||||||||||||||||||||
| if mask := frontendReq.GetUpdateMask(); mask != nil { | ||||||||||||||||||||||||
| _, hasStartDelayInMask = util.ParseFieldMask(mask)["startDelay"] | ||||||||||||||||||||||||
| updateFields = util.ParseFieldMask(mask) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if !frontendReq.GetRestoreOriginal() && hasStartDelayInMask { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // start_delay updates are only valid while the activity is still in its delay window. | ||||||||||||||||||||||||
| _, hasStartDelayInMask := updateFields["startDelay"] | ||||||||||||||||||||||||
| if hasStartDelayInMask { | ||||||||||||||||||||||||
| newDelay := frontendReq.GetActivityOptions().GetStartDelay() | ||||||||||||||||||||||||
| if err := validateStartDelay(newDelay); err != nil { | ||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||
|
|
@@ -663,6 +665,9 @@ func (a *Activity) UpdateActivityExecutionOptions( | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| attempt := a.LastAttempt.Get(ctx) | ||||||||||||||||||||||||
| policyRetryIntervalBeforeUpdate := backoff.CalculateExponentialRetryInterval(a.RetryPolicy, attempt.GetCount()-1) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if frontendReq.GetRestoreOriginal() { | ||||||||||||||||||||||||
| ogOptions := a.GetOriginalOptions() | ||||||||||||||||||||||||
| a.TaskQueue = common.CloneProto(ogOptions.GetTaskQueue()) | ||||||||||||||||||||||||
|
|
@@ -683,15 +688,14 @@ func (a *Activity) UpdateActivityExecutionOptions( | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| attempt := a.LastAttempt.Get(ctx) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Recalculate the retry interval based on the (possibly updated) retry policy. | ||||||||||||||||||||||||
| // This ensures a shortened retry interval takes effect immediately on re-dispatch. | ||||||||||||||||||||||||
| status := a.GetStatus() | ||||||||||||||||||||||||
| if (status == activitypb.ACTIVITY_EXECUTION_STATUS_SCHEDULED || | ||||||||||||||||||||||||
| status == activitypb.ACTIVITY_EXECUTION_STATUS_PAUSED) && | ||||||||||||||||||||||||
| attempt.GetCurrentRetryInterval() != nil { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Recalculate policy-derived retry intervals based on the (possibly updated) retry policy. | ||||||||||||||||||||||||
| // Worker-provided NextRetryDelay values are preserved for their already-scheduled retry. | ||||||||||||||||||||||||
| if a.shouldRecalculateCurrentRetryInterval( | ||||||||||||||||||||||||
| attempt, | ||||||||||||||||||||||||
| frontendReq.GetRestoreOriginal(), | ||||||||||||||||||||||||
| updateFields, | ||||||||||||||||||||||||
| policyRetryIntervalBeforeUpdate, | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| newInterval := backoff.CalculateExponentialRetryInterval(a.RetryPolicy, attempt.GetCount()-1) | ||||||||||||||||||||||||
| attempt.CurrentRetryInterval = durationpb.New(newInterval) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
@@ -728,6 +732,52 @@ func (a *Activity) UpdateActivityExecutionOptions( | |||||||||||||||||||||||
| }, nil | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // shouldRecalculateCurrentRetryInterval reports whether the pending retry's CurrentRetryInterval | ||||||||||||||||||||||||
| // should be re-derived from the (possibly updated) retry policy. All of the following must hold: | ||||||||||||||||||||||||
| // | ||||||||||||||||||||||||
| // - the activity is waiting to retry, i.e. in SCHEDULED or PAUSED state (a running attempt has no | ||||||||||||||||||||||||
| // pending backoff to recalculate); | ||||||||||||||||||||||||
| // - a retry is actually pending (CurrentRetryInterval is set); | ||||||||||||||||||||||||
| // - the update touches the retry policy: either RestoreOriginal (which replaces the whole policy), | ||||||||||||||||||||||||
| // or the update mask includes "retryPolicy" or one of its subfields; | ||||||||||||||||||||||||
| // - the pending interval is still policy-derived, i.e. it equals the pre-update policy value. | ||||||||||||||||||||||||
| // A mismatch means the interval is a worker-provided NextRetryDelay override, which we preserve. | ||||||||||||||||||||||||
| func (a *Activity) shouldRecalculateCurrentRetryInterval( | ||||||||||||||||||||||||
| attempt *activitypb.ActivityAttemptState, | ||||||||||||||||||||||||
| restoreOriginal bool, | ||||||||||||||||||||||||
| updateFields map[string]struct{}, | ||||||||||||||||||||||||
| policyRetryIntervalBeforeUpdate time.Duration, | ||||||||||||||||||||||||
| ) bool { | ||||||||||||||||||||||||
| status := a.GetStatus() | ||||||||||||||||||||||||
| if status != activitypb.ACTIVITY_EXECUTION_STATUS_SCHEDULED && | ||||||||||||||||||||||||
| status != activitypb.ACTIVITY_EXECUTION_STATUS_PAUSED { | ||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| currentRetryInterval := attempt.GetCurrentRetryInterval() | ||||||||||||||||||||||||
| if currentRetryInterval == nil { | ||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if !restoreOriginal { | ||||||||||||||||||||||||
| hasRetryPolicyInMask := false | ||||||||||||||||||||||||
| for field := range updateFields { | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're not strongly opinonated on this, I'd like to keep what I have as I feel it's shorter and more readable |
||||||||||||||||||||||||
| if field == "retryPolicy" || strings.HasPrefix(field, "retryPolicy.") { | ||||||||||||||||||||||||
| hasRetryPolicyInMask = true | ||||||||||||||||||||||||
| break | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if !hasRetryPolicyInMask { | ||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // CurrentRetryInterval stores either policy-derived backoff or worker-provided NextRetryDelay overrides. | ||||||||||||||||||||||||
| // Only recalculate intervals that match the old policy value; different values are treated as explicit | ||||||||||||||||||||||||
| // worker overrides. | ||||||||||||||||||||||||
| return currentRetryInterval.AsDuration() == policyRetryIntervalBeforeUpdate | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you send consecutive updates and recompute the retry interval on the second one? The currentRetryInterval here is just the last computed one I think, not the original retry interval from before a potential first retry interval created.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point out. Consecutive updates do recompute correctly. policyRetryIntervalBeforeUpdate is recomputed each call from the current a.RetryPolicy, and each update persists both the new policy and the recalculated interval, so they stay in sync. The second update compares against update 1's value (not the original) and recalculates as expected. I added a test to cover this case |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // mergeActivityOptions applies the field mask from the request to the activity state. | ||||||||||||||||||||||||
| // The structure mirrors the field-mask logic in service/history/api/updateactivityoptions/api.go | ||||||||||||||||||||||||
| func (a *Activity) mergeActivityOptions( | ||||||||||||||||||||||||
|
|
@@ -897,6 +947,8 @@ func (a *Activity) handleUnpauseRequested(ctx chasm.MutableContext, req *activit | |||||||||||||||||||||||
| if err := TransitionUnpausedWhilePauseRequested.Apply(a, ctx, event); err != nil { | ||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| case activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED: | ||||||||||||||||||||||||
| a.ResetKeepPaused = false | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return nil, serviceerror.NewFailedPreconditionf("activity is in non-unpausable state %v", a.GetStatus()) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
@@ -911,6 +963,8 @@ func (a *Activity) isPaused() bool { | |||||||||||||||||||||||
| case activitypb.ACTIVITY_EXECUTION_STATUS_PAUSED, | ||||||||||||||||||||||||
| activitypb.ACTIVITY_EXECUTION_STATUS_PAUSE_REQUESTED: | ||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||
| case activitypb.ACTIVITY_EXECUTION_STATUS_RESET_REQUESTED: | ||||||||||||||||||||||||
| return a.GetResetKeepPaused() | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
@@ -921,17 +975,16 @@ func (a *Activity) unpause( | |||||||||||||||||||||||
| event unpauseEvent, | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| attempt := a.LastAttempt.Get(ctx) | ||||||||||||||||||||||||
| // Compute the dispatch time while the pending retry state is still intact; it is cleared below. | ||||||||||||||||||||||||
| dispatchTime := a.unpauseDispatchTime(ctx, event) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if event.req.GetResetAttempts() { | ||||||||||||||||||||||||
| attempt.Count = 1 | ||||||||||||||||||||||||
| attempt.CurrentRetryInterval = nil | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if event.req.GetResetHeartbeat() { | ||||||||||||||||||||||||
| a.LastHeartbeat = chasm.NewDataField(ctx, &activitypb.ActivityHeartbeatState{}) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| attempt.Stamp++ | ||||||||||||||||||||||||
| attempt.CurrentRetryInterval = nil | ||||||||||||||||||||||||
| if timeout := a.GetScheduleToStartTimeout().AsDuration(); timeout > 0 { | ||||||||||||||||||||||||
| ctx.AddTask( | ||||||||||||||||||||||||
| a, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc comment for the logic here would be helpful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as I understand it we recalc if all of the following are true:
Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added docs.
Mostly, with corrections: