Skip to content

Go + Python: honor per-operation retry.max as a ceiling; add parity guard - #483

Merged
jeremy merged 2 commits into
mainfrom
feat/perop-retry-max-go-python
Jul 28, 2026
Merged

Go + Python: honor per-operation retry.max as a ceiling; add parity guard#483
jeremy merged 2 commits into
mainfrom
feat/perop-retry-max-go-python

Conversation

@jeremy

@jeremy jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member

Problem (A4b2)

Every operation carries a per-op retry.max in behavior-model.json (183 ops at 3, 43 at 2). TypeScript, Swift, and Kotlin honor it; generated Go and Python did not — Python shipped it in metadata.json as dead data, Go emitted no per-op table. So a customized Go/Python client retried the more side-effect-sensitive ops more times than the other SDKs.

Fix — per-op ceiling min(client_cap, op_max)

The per-op field is named max, so it composes with the client cap by taking the smaller (an upper bound on attempts). TypeScript and Swift expose no numeric client cap (only an on/off) and use op_max directly. Kotlin does expose BasecampConfig.maxRetries, but resolves opRetry?.maxRetries ?: config.maxRetries — the per-op value overrides rather than bounds the client cap, so it behaves as a floor (a divergence from the ceiling contract, tracked in #485). Because every op's max ≤ the default cap of 3, min() makes a default-or-raised Go/Python client produce exactly the per-op attempt count (parity), while uniquely honoring a client that lowered its cap to 0/1 to disable/restrict retries.

  • Go: emit a separate operationRetryMax map (opId → max) from x-basecamp-retry.maxAttempts, plus an additive GetOperationRetryMax(operationId) accessor; doWithRetry reads it and applies the ceiling by operationId. Kept off the exported OperationMetadata struct so wiring it up doesn't change that struct's shape (which would break external unkeyed composite literals) — OperationMetadata is byte-identical to main.
  • Python: thread the canonical (PascalCase) operationId into _request_with_retry (sync + async) for every retryable path. Notably the generated GET/list/pagination methods now pass operation="<OpId>" too — previously they passed nothing or the snake_case OperationInfo.operation (not a metadata key), so the ceiling silently never applied to the 118 GET ops.

⚠️ Observable behavior change (release-noted in SPEC)

  • Default client (max_retries = 3): only the 9 idempotent max:2 ops (account/gauge/preference writes) change — they now retry at most twice instead of three times.
  • Client that raised its cap above 3: all 187 retry-eligible ops are now clamped to their per-op max (the intended meaning of a per-op ceiling, matching TS/Swift/Kotlin).
  • Client that lowered its cap to 0/1: unchanged.

A4c — retry-metadata parity guard

New scripts/check-retry-metadata-parity.py (wired into make check):

  • Criterion 1 (static): emitted per-op retry values EQUAL behavior-model.json across all six emitters (Python/Ruby/TS/Kotlin/Swift full tuple; Go max-only from the operationRetryMax map — Go consumes only max, so emitting the full inert tuple would be dead data). Fails on missing and stale/extra ops. Distinct from the regenerate-and-diff freshness gates.
  • Criterion 2 (token-smoke classification, not behavioral proof): usage-form token presence/absence per consuming source classifies TS/Swift/Kotlin = full tuple, Go/Python = max only, Ruby = none. Behavioral proof lives in each SDK's retry tests.

Tests / Verify

  • Go per-op ceiling test + Python sync/async ceiling tests, each red-proofed.
  • Integration tests through real generated services + the real bundled metadata (projects.listListProjects max:3; account.update_account_nameUpdateAccountName max:2), proven to fail against the snake_case-key bug (5 attempts vs 3).
  • make py-check (480 tests, mypy, ruff, drift), Go drift + tests, and the parity guard (self-tested against injected stale/extra entries) all green.

Part of the retry-contract follow-up (#456 / #460 / #461 / #476). Shares go/templates/client.tmpl with #481; whichever merges second should be rebased and regenerated (make -C go generate) — disjoint regions, no textual conflict.

Copilot AI review requested due to automatic review settings July 28, 2026 06:28
@jeremy jeremy added bug Something isn't working go spec Changes to the Smithy spec or OpenAPI python Pull requests that update the Python SDK labels Jul 28, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed spec Changes to the Smithy spec or OpenAPI documentation Improvements or additions to documentation labels Jul 28, 2026
@jeremy

jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Series — retry-contract follow-up (branch fresh off main, each independent):

#481 and #483 both touch go/templates/client.tmpl (disjoint regions, no textual conflict). Whichever merges second should be rebased and regenerated (make -C go generate); the merged tree passes the Go generated-drift gate.

@jeremy jeremy added the spec Changes to the Smithy spec or OpenAPI label Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR aligns the Go and Python SDKs’ retry behavior with the repo’s per-operation retry contract by enforcing retry.max as an operation-specific ceiling (effective_attempts = min(client_cap, op_max)), and adds a cross-SDK parity guard to prevent future drift from behavior-model.json.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Changes:

  • Go: emit per-operation RetryMax from x-basecamp-retry.maxAttempts and clamp retry attempts by operationId in doWithRetry.
  • Python: plumb canonical PascalCase operationId into the transport for GET/list/pagination and apply the same per-op ceiling in sync + async HTTP clients.
  • Add scripts/check-retry-metadata-parity.py and wire it into make check; document the behavior in SPEC.md and add tests validating the ceiling.

Reviewed changes

Copilot reviewed 9 out of 54 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
SPEC.md Documents the per-operation retry ceiling semantics and observable behavior changes.
scripts/check-retry-metadata-parity.py New guard: validates emitted retry metadata equals behavior-model.json and token-smoke checks runtime consumption sites.
Makefile Adds check-retry-metadata-parity to make check.
go/templates/client.tmpl Generates OperationMetadata.RetryMax and applies the per-op ceiling in the retry loop.
go/pkg/generated/client.gen.go Regenerated Go client reflecting the new per-op retry ceiling metadata + logic.
go/pkg/basecamp/generated_perop_retry_test.go Adds Go tests pinning per-op ceiling behavior against real generated metadata.
python/src/basecamp/_http.py Adds operation plumb-through and applies per-op retry ceiling in sync transport retry loop.
python/src/basecamp/_async_http.py Adds operation plumb-through and applies per-op retry ceiling in async transport retry loop.
python/src/basecamp/generated/services/_base.py Ensures sync generated services pass operation through GET/list/pagination paths.
python/src/basecamp/generated/services/_async_base.py Ensures async generated services pass operation through GET/list/pagination paths.
python/scripts/generate_services.py Generator now always emits canonical operation="<OperationId>" kwarg (including GET/list/pagination).
python/tests/test_http.py Adds unit + integration tests proving per-op ceilings apply (sync + async) via real generated services + bundled metadata.
python/src/basecamp/generated/services/account.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/automation.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/boosts.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/campfires.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/card_columns.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/card_steps.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/card_tables.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/cards.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/checkins.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/client_approvals.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/client_correspondences.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/client_replies.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/comments.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/documents.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/events.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/everything.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/forwards.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/gauges.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/hill_charts.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/message_boards.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/message_types.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/messages.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/my_assignments.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/my_notifications.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/people.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/projects.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/recordings.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/reports.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/schedules.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/search.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/subscriptions.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/templates.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/timeline.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/timesheets.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/todolist_groups.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/todolists.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/todos.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/todosets.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/tools.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/uploads.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/vaults.py Passes canonical operation IDs into transport calls for per-op ceiling.
python/src/basecamp/generated/services/webhooks_service.py Passes canonical operation IDs into transport calls for per-op ceiling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/check-retry-metadata-parity.py
Comment thread go/pkg/basecamp/generated_perop_retry_test.go

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 54 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/check-retry-metadata-parity.py
Comment thread go/pkg/basecamp/generated_perop_retry_test.go Outdated
Copilot AI review requested due to automatic review settings July 28, 2026 06:50
@jeremy
jeremy force-pushed the feat/perop-retry-max-go-python branch from 8fc4d5c to 07533ae Compare July 28, 2026 06:50
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed spec Changes to the Smithy spec or OpenAPI labels Jul 28, 2026
@jeremy

jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Addressed the bot review:

  • [Copilot] Fixed the unused ro variable (tuple now uses r[ro]) and corrected the "lower bound" test comment to "ceiling (upper bound on attempts)".
  • [cubic P2] The parity guard now fails on stale/extra Go RetryMax entries (ops present in the Go table but absent from behavior-model.json), matching check_full_tuple — self-tested against an injected stale entry.

Guard green on both criteria; Go drift/tests green.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 54 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

scripts/check-retry-metadata-parity.py:56

  • This script prints Unicode symbols (e.g., ✗/✓). On systems where Python’s stdout encoding is ASCII (common when locale is C/POSIX), these prints can raise UnicodeEncodeError and make make check fail even when the parity checks pass.

Consider forcing UTF-8 for stdout (or switching to ASCII-only output) so the guard is robust across environments.
scripts/check-retry-metadata-parity.py:215

  • Criterion 2’s “token-smoke” check uses exact substring tokens that include whitespace (e.g., "baseDelayMs ??"). This is brittle: harmless formatting changes in the consuming file (like removing spaces) can break make check even though runtime consumption is unchanged.

Prefer whitespace-insensitive tokens (e.g., retryConfig.baseDelayMs) or regex-based matching.
go/templates/client.tmpl:579

  • OperationMetadata is exported and returned from GetOperationMetadata. Adding a new exported field (RetryMax) is a source-breaking change for downstream users who construct OperationMetadata with positional composite literals (e.g., generated.OperationMetadata{true, false}), because Go requires updating those literals when struct fields change.

If maintaining Go source compatibility is important, consider keeping OperationMetadata’s public shape stable (e.g., store per-op retry max in a separate internal map or add a separate accessor like GetOperationRetryMax(opID)), or at minimum call out the breaking change explicitly in release notes.

	// RetryMax is the per-operation maximum TOTAL attempt count from the
	// x-basecamp-retry extension (0 when the operation carries no override, in
	// which case the client-configured cap applies unchanged). doWithRetry uses
	// it as a per-operation ceiling: effective attempts = min(client cap, RetryMax).
	RetryMax int

Copilot AI review requested due to automatic review settings July 28, 2026 07:20
@jeremy
jeremy force-pushed the feat/perop-retry-max-go-python branch from 07533ae to 7a4c329 Compare July 28, 2026 07:20
@github-actions github-actions Bot removed the documentation Improvements or additions to documentation label Jul 28, 2026
@jeremy

jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Addressed the exported-API concern:

  • [Go source break] RetryMax is no longer a field on the exported OperationMetadata struct (which would break external unkeyed composite literals). The per-op ceiling now lives in a separate operationRetryMax map with a new additive GetOperationRetryMax(operationId) accessor. OperationMetadata is byte-identical to main, so there is no source break and no breaking label is needed. The retry-count behavior change for cap>3 clients remains release-noted in SPEC.
  • The A4c guard reads the new map and still fails on stale/extra Go entries (self-tested); drift/tests/vet/lint green.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 54 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

go/pkg/basecamp/generated_perop_retry_test.go:20

  • The comment claims per-operation retry max is emitted into operationMetadata.RetryMax, but the generated client actually emits it via the separate operationRetryMax table (exposed by generated.GetOperationRetryMax). This is misleading for future maintainers reading the tests.
// These tests pin the generated client's per-operation retry ceiling. The
// x-basecamp-retry.maxAttempts value for each operation is emitted into
// operationMetadata.RetryMax, and doWithRetry applies it as a ceiling (upper
// bound on attempts): effective attempts = min(client cap, RetryMax). This
// matches how TS/Swift/

scripts/check-retry-metadata-parity.py:25

  • The docstring says Go emits the per-op max as operationMetadata.RetryMax, but this PR actually emits it via the separate operationRetryMax map in go/pkg/generated/client.gen.go (and GetOperationRetryMax). The comment should reflect the real output so the parity guard remains understandable.

…uard

A4b2 — generated Go and Python drove their retry loops from the client-wide
cap only, ignoring the per-operation retry.max that TS/Swift/Kotlin already
honor (Python shipped it in metadata.json as dead data; Go emitted no per-op
table). Wire the per-op value in as a ceiling (upper bound on attempts):

    effective_attempts = min(client_cap, op_max)

Precedence rationale: the per-op field is named `max`, so it composes with the
client cap by taking the smaller. TS/Swift/Kotlin expose no numeric client cap
and use op_max directly; because every op's max is <= the default cap of 3,
min() makes a default-or-raised Go/Python client produce exactly the per-op
attempt count (parity), while uniquely honoring a Go/Python client that lowered
its cap to 0/1 to disable/restrict retries. Observable change: a default client
sees only the 9 idempotent max:2 ops drop 3->2; a client that RAISED its cap
above 3 now sees all 187 retry-eligible ops clamped to their per-op max
(release-noted in SPEC).

  - Go: emit OperationMetadata.RetryMax from x-basecamp-retry.maxAttempts
    (client.tmpl) and apply the ceiling in doWithRetry by operationId.
  - Python: thread the CANONICAL (PascalCase) operationId into
    _request_with_retry (sync + async) for every retryable path. Crucially the
    generated GET/list/pagination methods now pass operation="<OpId>" too —
    previously they passed nothing or OperationInfo.operation (snake_case, not a
    metadata key), so the ceiling silently never applied to the 118 GET ops.
    generate_services.py now emits the operation= kwarg on GET/list/paginated
    calls; _base/_async_base thread it through _request_list and all three
    pagination wrappers.

A4c — add scripts/check-retry-metadata-parity.py, wired into `make check`.
Criterion 1 (static): emitted per-op retry values EQUAL behavior-model.json
across all six emitters (Python/Ruby/TS/Kotlin/Swift full tuple; Go max only —
a documented design deviation: Go consumes only max, so emitting the full inert
tuple into a Go struct would be dead data). Criterion 2 (token-smoke
classification, NOT behavioral proof): usage-form token presence/absence per
consuming source classifies TS/Swift/Kotlin=full tuple, Go/Python=max only,
Ruby=none, and catches gross removal of a consumption site (behavioral proof
lives in each SDK's retry tests).

Tests (outside pkg/generated): Go ceiling test proven to fail without the
min() application; Python sync+async ceiling tests; and INTEGRATION tests
through real generated services with the real bundled metadata.json
(projects.list -> ListProjects max:3; account.update_account_name ->
UpdateAccountName max:2), proven to fail against the snake_case-key bug (5
attempts vs 3). Parity guard self-tested against mutated values.

Follows the retry-contract program (#456/#460/#461/#476).
@jeremy jeremy added the breaking Breaking change to public API label Jul 28, 2026
Copilot AI review requested due to automatic review settings July 28, 2026 07:52
@jeremy
jeremy force-pushed the feat/perop-retry-max-go-python branch from 7a4c329 to aee8bd9 Compare July 28, 2026 07:52
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 28, 2026
@jeremy

jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Corrected the terminology so the public explanation matches the code:

  • The Go per-op ceiling lives in the operationRetryMax map (read via GetOperationRetryMax), NOT a field on OperationMetadata. Updated the PR body, the test comment, and the guard docstring + output label accordingly.
  • Also labeled breaking to match the repo classifier's treatment of retry-behavior changes (the cap>3 clamping is observable). The source-compat concern is resolved — OperationMetadata is byte-identical to main.

@jeremy jeremy added the spec Changes to the Smithy spec or OpenAPI label Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 54 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

python/tests/test_http.py:505

  • client.close() is not guaranteed to run if the test fails before reaching it (e.g., assertion failure). Use try/finally so the client is always closed.
    python/tests/test_http.py:525
  • Ensure await client.close() runs even when the test fails early by using try/finally.
    python/tests/test_http.py:496
  • client.close() is not in a finally block, so if the assertion fails (or an unexpected exception is raised) the underlying httpx client may not be closed, which can leak resources and cause noisy warnings in the test suite. Wrap the call in try/finally so cleanup always happens.

This issue also appears on line 501 of the same file.
python/tests/test_http.py:516

  • await client.close() should be executed even if the assertion fails; otherwise the async client can be left unclosed. Wrap the body in try/finally.

This issue also appears on line 521 of the same file.

… cap

The paragraph claimed TypeScript, Swift, and Kotlin "expose no numeric
client-wide cap, only an on/off". True for TypeScript and Swift, which
have only enableRetry. False for Kotlin: BasecampConfig.maxRetries is an
Int, and BasecampHttpClient resolves the two as
`opRetry?.maxRetries ?: config.maxRetries` — the per-op value overrides
the client's instead of bounding it, so a Kotlin caller who lowers
maxRetries to 1 still gets the operation's count.

That makes Kotlin the SDK where the ceiling genuinely behaves as a floor,
which is what #485 tracks. Ruby has the opposite gap: it bounds by
config.max_retries alone and never enforces the per-op max.

Rationale only — no behavior change.
Copilot AI review requested due to automatic review settings July 28, 2026 19:55
@jeremy

jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Corrected the cross-SDK rationale in SPEC.md — it contradicted #485.

The paragraph said TypeScript, Swift, and Kotlin "expose no numeric client-wide cap, only an on/off". That is true for TypeScript (enableRetry?: boolean) and Swift (enableRetry: Bool), and false for Kotlin: BasecampConfig.maxRetries is an Int (BasecampConfig.kt:24), and BasecampHttpClient resolves the two as

val maxAttempts = opRetry?.maxRetries ?: config.maxRetries

so the per-op value overrides the client's rather than bounding it. Because every operation has a retry block, config.maxRetries is effectively dead for retry sizing — a Kotlin caller who sets it to 1 still gets the operation's count.

That makes Kotlin the SDK where the ceiling genuinely behaves as a floor. Ruby has the opposite gap: it bounds by config.max_retries alone and never enforces the per-op max.

I also overstated this in #485 by lumping TypeScript and Swift in with Kotlin — with no numeric cap there is nothing to override, so the finding is vacuous for those two. Correcting that issue too.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 54 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

python/src/basecamp/_http.py:323

  • isinstance(op_max, int) will treat booleans as ints (True == 1, False == 0). If a caller supplies custom metadata (e.g., in tests or advanced usage) and accidentally sets retry.max to a boolean, this will silently clamp retries incorrectly. Prefer a strict int type check here.
        op_max = self._metadata.get(operation, {}).get("retry", {}).get("max")
        if isinstance(op_max, int) and 0 < op_max < max_attempts:
            return op_max

python/src/basecamp/_async_http.py:323

  • isinstance(op_max, int) will treat booleans as ints (True == 1, False == 0). If a caller supplies custom metadata and accidentally sets retry.max to a boolean, this will silently clamp retries incorrectly. Prefer a strict int type check here.
        op_max = self._metadata.get(operation, {}).get("retry", {}).get("max")
        if isinstance(op_max, int) and 0 < op_max < max_attempts:
            return op_max

go/templates/client.tmpl:628

  • The ok return value here is described as "false when the operation carries no per-op override", but operationRetryMax is emitted with a positive max for every known operation, so ok is only false for an unknown/unrecognized operationId. This comment is misleading for callers relying on ok semantics.
// GetOperationRetryMax returns the per-operation retry ceiling (maximum total
// attempts) for the given operation ID. ok is false when the operation carries
// no per-op override, in which case the client-configured cap applies unchanged.

@jeremy jeremy added the spec Changes to the Smithy spec or OpenAPI label Jul 28, 2026
@jeremy
jeremy merged commit 20d8430 into main Jul 28, 2026
48 checks passed
@jeremy
jeremy deleted the feat/perop-retry-max-go-python branch July 28, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change to public API bug Something isn't working documentation Improvements or additions to documentation go python Pull requests that update the Python SDK spec Changes to the Smithy spec or OpenAPI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants