fix: Update SDK, ensure idempotency keys are unique and don't retry http 4xx - #208
Conversation
✅ Approve — automated reviewNo actionable correctness regressions were found in the current diff. Previously reported NumaryBot findings are resolved or were explicitly dismissed. No findings. |
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #208 (comment)
|
This PR adds a payments-version probe that dispatches transfer initiation between the v3 payment-initiations API and the legacy v1 API, classifies SDK errors as non-retryable Temporal application errors, self-heals v3 CONFLICT via fetch-by-reference, and client-side resolves connector IDs from provider on both paths. The design is sound and the v3 path fully achieves the stated goal ("use payments v3 when possible, don't retry 4xx"). However, on the v1 fallback and the legacy StandardsNo documented coding standards were found to apply to this diff, and no tooling-enforceable violations exist. One confirmed judgement-call finding, low severity:
Spec
Reviewed independently by GLM (glm-5.3-flash) and Codex (gpt-5.6-sol) via Shipfox; verified and synthesized by GLM. |
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #208 (comment)
|
This PR upgrades formance-sdk-go to v3.8.1, adds a payments-version gate that switches between the v3 and legacy v1 transfer-initiation APIs, classifies SDK errors as retryable/non-retryable, and adds conflict self-healing with tests. The core v3 path is well designed and well tested, but several 4xx error paths still retry indefinitely and the new v1 fallback contradicts the PR's own idempotency-key fix, so as written it does not fully deliver its stated goal ("Return non-retriable errors for http status 4xx", commit 62ae544). Recommendation: request changes on the retry-classification gaps below; the Standards axis has no confirmed material finding. Spec
StandardsNo confirmed material finding. The documented standards ( Reviewed independently by GLM (glm-5.3-flash) and Codex (gpt-5.6-sol) via Shipfox; verified and synthesized by GLM. |
|
This PR reworks payment initiation to pick between the payments v3 and v1 APIs based on the target stack's reported payments version, adds error classification (4xx → non-retryable), a bounded retry context, conflict self-healing on v3, and client-side connector resolution to replace the removed Recommendation: request changes (one blocking concern, one non-blocking). Standards
Other candidate smells reviewed and rejected as non-material: the SpecNo spec is available for this change (the PR references an external Linear ticket, TS-581, which is not in the repository), so this axis has no confirmed material finding. Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
|
This PR replaces the ad-hoc v1 transfer-initiation call with a version-aware dual path (v3 payment-initiations with conflict self-healing, v1 fallback for pre-v3 payments stacks), adds client-side connector resolution now that the SDK dropped the Standards
SpecNo spec available — the PR body contains only "Relates to TS-581" (a Linear ticket not reachable via the available integrations), and no issue reference or spec file matching the branch was found. The Spec axis therefore has no confirmed material finding. Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #208 (comment)
… to be in pending state
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot review complete: no remaining inline findings.
Resolved 1 stale NumaryBot review thread (1 fixed, 0 outdated).
Summary: #208 (comment)
|
This PR gates Standards
SpecNo spec available for this PR (the referenced Linear ticket TS-581 is not accessible), so this axis has no confirmed material finding. Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #208 (comment)
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot review complete: no remaining inline findings.
Resolved 1 stale NumaryBot review thread (1 fixed, 0 outdated).
Summary: #208 (comment)
|
This PR migrates the codebase to formance-sdk-go v5, drops the silently-failing v3 payments path, makes payments 4xx errors non-retryable ( StandardsNo documented standards govern this diff (no CODING_STANDARDS.md/AGENTS.md;
SpecNo spec is available for this PR (the referenced ticket TS-581 is an external Linear item not reachable here), so no spec-axis findings could be confirmed. Explicitly: Spec has no confirmed material finding — the Spec axis is empty. Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
|
This PR migrates the codebase from formance-sdk-go v3 to v5, reinstates a CONFLICT self-heal path for payment transfer initiations (fetch-by-reference + reclassify instead of blindly retrying), adds client-side connector resolution from Standards
Candidates rejected during verification: the ledger-defaulting triplication in SpecNo spec is available for this PR (the referenced Linear ticket TS-581 is not fetchable), so the Spec axis has no confirmed material finding. Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
The v1/v2 GET /connectors response decodes into a closed Connector enum (STRIPE ... GENERIC), but payments' v2 handler passes any provider outside that set through verbatim - toV2Provider maps only the legacy names and its default branch returns the v3 provider name as-is. One connector for a newer provider therefore fails the whole response with "invalid value for Connector: routable", taking payouts to every other PSP on that stack down with it. Resolve the connector through v3's filtered listing wherever it exists, whose provider is a plain string, and keep the v1 listing only for stacks that predate the v3 API - those can only have the providers the enum declares.
|
This PR migrates to formance-sdk-go v5, makes transfer-initiation idempotency keys unique per Temporal run, classifies payments 4xx errors as non-retryable, adds a CONFLICT self-heal path for transfer initiations, fixes StandardsNo documented standards exist for these files (
SpecThe PR body (a load-test findings/validation report) is the only spec; its implied requirements — SDK v5 migration, RunID-based unique references, no retry on 4xx, CONFLICT self-heal,
Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
This reverts commit efde923.
|
This PR migrates orchestration from formance-sdk-go v3 to v5 (type/package reshuffles, Standards
The remaining candidates from both review passes (duplicated SpecNo confirmed material finding — no spec is available for this change, so the spec axis could not be evaluated. Reviewed independently by GLM (glm-5.3-flash) and DeepSeek (deepseek-v4-pro-0813) via Shipfox; verified and synthesized by GLM. |
Relates to TS-581
Load Test 3 Findings — Payment Initiation via Orchestration (Self-Heal + InsertNewStage Fix)
Stack:
knonmzexcoal-bblpWindow:
2026-09-07T19:56:59Z→2026-09-07T20:29:09Z(launch window; workflows continued resolving until ~20:52Z)Batch size: 500 requests (475 valid / 25 invalid, 95%/5% split), base refs
loadtest3-valid-N/loadtest3-invalid-N, actual refs<base>-<8charhexuuid>— every fctl invocation, including retries, got a freshly generated UUID suffix so no request could ever collide with another on Routable'sexternal_id.Image under test:
orchestration:bfe9fb9efb13290e9d20365db3eeed16c8751423— contains theInsertNewStageON CONFLICT DO NOTHINGidempotency fix and thecreateTransferInitiationWithSelfHealCONFLICT-recovery fix (plus a same-behavior refactor: shared self-heal helper,context.godocstring/list cleanup, untrackedcoverage.txt).1. Payment-initiation status breakdown (payments API)
All 500 refs found (100%, zero missing). Zero "external_id already exists" failures — the UUID-uniqueness fix worked as intended.
All 13 invalid-cohort FAILED records carry the correct, expected reason:
create payable: acting_team_member is required. No unexpected failure reasons appeared anywhere in the batch.2. Signoz error breakdown — V1 create endpoint (
v2_transferInitiationsCreate)object already exists(CONFLICT, self-healed)context canceled(client-side StartToCloseTimeout firing)498
context cancelederrors across ~500 workflows means almost every workflow's firstCreateTransferInitiationattempt hit the 60s timeout — this is not a rare edge case at this load level, it's close to universal. Each of those timed-out-but-actually-successful writes then produces a CONFLICT on the next attempt (3,474 of them, ~7 per workflow), all correctly resolved bycreateTransferInitiationWithSelfHeal.3. Temporal workflow completion times (
RunSend)Completion-time percentiles (n=473 completed by final check):
Success rate among resolved workflows: 471/476 = 98.9% (vs. loadtest2's 1/500 = 0.2%).
4.
CreateTransferInitiationretry countDirectly inspected 15 workflows via Temporal history (exact attempt numbers from
ActivityTaskStarted.attempt): attempts of 2, 2, 5, 7, 7, 7, 8, 8, 9, 9, 9, 9, 9, 10 — avg 7.33 in that sample. The scheduled-to-last-start gap for every one of these matched, to within ~0.2s, the formula(attempts−1)×60s + cumulative RetryPolicy backoff(2s, 4s, 8s... doubling, capped at 200s) — confirming every failed attempt is genuinely consuming the full 60sStartToCloseTimeout, not queueing or something else.Using that verified formula to estimate attempts from duration across all 473 completed workflows (a duration-based estimate, cross-validated against the 15 directly-inspected exact values above): average ≈ 7.8 attempts, median 9, distribution:
Self-heal was extensively exercised — 3,474 CONFLICT resolutions across the batch, essentially once per attempt beyond the first for nearly every workflow, all resolving to success (or, in one directly-inspected case, correctly surfacing a genuine terminal failure — see below).
One directly-inspected invalid-cohort failure (
658e0895-ebe0-4fea-9b6c-bc10f1e3fda1-0) shows the self-heal path working exactly as designed on a genuine terminal failure: attempt 1 created the transfer initiation, Routable rejected it for the missingacting_team_member, the retry hit CONFLICT,getTransferInitiationByReferencefetched the record, found it in a terminalFAILEDstate, andclassifyExistingTransferInitiationcorrectly returned a non-retryable error carrying the real reason (acting_team_member is required).5. Three-way comparison
CreateTransferInitiationattemptsThe regression is resolved: the fixes work as intended.
InsertNewStage'sON CONFLICT DO NOTHINGeliminated the infinite-retry-on-23505 hang, andcreateTransferInitiationWithSelfHealturned "timeout-then-CONFLICT" from a permanent failure back into a recoverable, eventually-successful path — restoring (and, via a correctly-surfaced terminal-failure case, slightly improving on) the old V3 path's behavior.