temporalio / temporalio/temporal

SyncState() ignores remainingAttempt — no retry limit enforced unlike Resend()

Open
#10,490 0 comments 0 reactions 1 assignee View on GitHub

@yux0 is already working on this.

Since Jun 11, 2026.

potential-bug
Dominant language
Go
Stars
23.2k
Forks
1.9k
Avg merge
2d 8h
Merged PRs (30d)
228

Description

Summary

SyncState() in executable_task.go accepts a remainingAttempt
parameter but never checks or decrements it, unlike Resend() which
correctly enforces the retry limit.

Root Cause

Resend() correctly handles remainingAttempt:

remainingAttempt--
if remainingAttempt < 0 {
    return false, ErrResendAttemptExceeded
}

SyncState() ignores it entirely:

// TODO: check & update remainingAttempt
// ... proceeds without any retry limit check

Impact

  • SyncState() can retry indefinitely on certain error types
  • Callers always pass ResendAttempt = 2 as a constant
  • The retry limit is never enforced for sync state operations
  • Inconsistent behavior between SyncState() and Resend()
  • Could cause excessive load on source cluster during sync failures

Affected Callers

All callers pass ResendAttempt constant (= 2) but it's never used:

  • executable_sync_versioned_transition_task.go:154
  • executable_verify_versioned_transition_task.go:288
  • executable_workflow_state_task.go:149, 181
  • executable_sync_hsm_task.go:154

Suggested Fix

Apply the same pattern as Resend():

func (e *ExecutableTaskImpl) SyncState(
    ctx context.Context,
    syncStateErr *serviceerrors.SyncState,
    remainingAttempt int,
) (bool, error) {
    remainingAttempt--
    if remainingAttempt < 0 {
        e.Logger.Error("sync state attempts exceeded", ...)
        return false, ErrResendAttemptExceeded
    }
    // ... rest of function
}

References

  • service/history/replication/executable_task.go:702 (TODO)
  • service/history/replication/executable_task.go:425-440 (Resend reference)
  • service/history/replication/executable_task.go:47 (ResendAttempt = 2)

Expected Behavior

SyncState() should enforce the retry limit passed via
remainingAttempt parameter, stopping retries and returning
ErrResendAttemptExceeded when the limit is reached —
consistent with how Resend() handles the same parameter.

Actual Behavior

SyncState() ignores the remainingAttempt parameter entirely.
Despite callers always passing ResendAttempt (= 2), no retry
limit is enforced. The function can retry indefinitely on
certain error types, unlike Resend() which correctly
decrements and checks the limit.

Steps to Reproduce the Problem

  1. Trigger a replication sync state operation that fails
    repeatedly (e.g. source cluster unavailable)
  2. Observe SyncState() retrying without limit
  3. Compare with Resend() which stops after 2 attempts
    and returns ErrResendAttemptExceeded

Specifications

  • Version: latest main
  • Platform: any
  • Affected file: service/history/replication/executable_task.go:702
  • Related: ResendAttempt constant (line 47), ErrResendAttemptExceeded (line 51)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.