Skip to content

Add optional retry policy for PayloadCodec failures #2370

Description

@whitemanthedj

Is your feature request related to a problem? Please describe.

A PayloadCodec may call an external service during Encode/Decode — examples include the in-tree NewRemotePayloadCodec (HTTP to a sidecar), KMS-backed encryption codecs that call a cloud KMS for data-key operations, and codecs that fetch schemas from a remote registry. Any of these can fail transiently (network blip, throttling, 5xx). Today the SDK has no way to retry a codec call: the first error from any codec immediately aborts the encode/decode and surfaces to user code. Each downstream user has to either fork the codec or wrap retry logic inside their own codec implementation, which duplicates effort and doesn't help the stdlib RemotePayloadCodec, which has no retries today (see converter/codec.goRemotePayloadCodec.encodeOrDecode does a single pc.options.Client.Do(req) and returns the error directly).

Describe the solution you'd like

A sibling constructor NewCodecDataConverterWithOptions(parent, codecs, CodecDataConverterOptions) mirroring the *WithOptions pattern used in PR #2225 (Serialization Context). The options struct exposes an optional retry policy applied per codec call:

type CodecDataConverterOptions struct {
    RetryPolicy *CodecRetryPolicy // nil => no retry (default)
    IsRetryable func(error) bool  // nil => retry all errors
    Context     context.Context   // nil => context.Background()
}

type CodecRetryPolicy struct {
    InitialInterval    time.Duration
    BackoffCoefficient float64
    MaximumInterval    time.Duration
    MaximumAttempts    int
    ExpirationInterval time.Duration
}

The existing NewCodecDataConverter(parent, codecs...) stays unchanged and delegates to the new one with a zero-value options struct, preserving backward compatibility (no retries by default).

Retry boundary: each codec's Encode/Decode is retried independently, so a chain of [compress, encrypt] re-encrypts on transient KMS failure without re-compressing (relevant for non-idempotent codecs like authenticated encryption).

Example usage

dc := converter.NewCodecDataConverterWithOptions(
    parent,
    []converter.PayloadCodec{kmsCodec},
    converter.CodecDataConverterOptions{
        RetryPolicy: &converter.CodecRetryPolicy{
            InitialInterval:    100 * time.Millisecond,
            BackoffCoefficient: 2.0,
            MaximumAttempts:    4,
        },
        IsRetryable: func(err error) bool {
            // codec-specific predicate, e.g. detect KMS throttling
            return isTransientKMSError(err)
        },
        Context: ctx,
    },
)

Describe alternatives you've considered

  • Each codec author implements retry internally. Works, but duplicates effort and doesn't fix RemotePayloadCodec.
  • Exposing internal/common/backoff publicly. Larger surface; out of proportion to the need.
  • Adding context.Context to PayloadCodec.Encode/Decode. Interface-breaking; separate discussion. The options-struct Context only governs retry sleeps, not the codec call itself.

Additional context

Related prior issues: #2206 (closed by #2228) hit exactly this scenario — a remote codec returned DeadlineExceeded during encodeArgs and, combined with a session activity cancellation, permanently bricked a workflow. PR #2228 fixed the catastrophic side-effect (no more panic; the workflow task fails gracefully and the server retries), but the underlying transient codec failure still kills the in-flight task and forces a full task retry. This proposal is the missing root-cause complement: codecs that opt in recover from a transient blip without burning a workflow task at all.

I'm happy to implement this and send up a PR, but ideally I want to confirm the API shape with maintainers before doing so. Primary open questions remaining are:

  1. The local CodecRetryPolicy struct as proposed, or should it thread something else (e.g. temporal.RetryPolicy)? A local struct avoids an import cycle from converter/ back into temporal/.
  2. Any objection to the per-codec (vs. whole-chain) retry boundary?
  3. Default IsRetryable semantics — retry-all (proposed) vs. retry-none (force explicit classification)?

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions