Skip to content

fix(providers/azure): restore IdempotencyToken threading in DoPurchaseTwoStep#729

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/721-azure-two-step-idempotency
May 26, 2026
Merged

fix(providers/azure): restore IdempotencyToken threading in DoPurchaseTwoStep#729
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/721-azure-two-step-idempotency

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 26, 2026

Closes #721 (regression of #641; unblocks #639).

Why Option B (caller-level guard), not Option A (client-supplied order ID)

The package docstring at purchase.go:19-26 documents that Azure rejects client-supplied reservationOrderId in the two-step flow — that's the very reason PR #680 had to switch to DoPurchaseTwoStep. Option A (restore IdempotencyGUID-derived order IDs from PR #653) is therefore not viable. Option B mirrors the AWS EC2 findRIByIdempotencyToken pattern: tag every purchase with the deterministic token, look up by tag before purchasing, short-circuit on match.

Implementation

Centralized in providers/azure/services/internal/reservations/purchase.go:

  1. ApplyPurchaseTags(body, source, token) — stamps both purchase-automation and cudly-idempotency-token tags into the request body.
  2. FindReservationOrderByIdempotencyToken(...) — lists reservation orders via Microsoft.Capacity/reservationOrders, filters by tag, skips terminal-failed states (Cancelled/Failed/Expired), follows nextLink pagination. Split into fetchReservationOrdersPage + matchReservationOrderInPage to stay under gocyclo:10.
  3. DoIdempotentPurchaseTwoStep(...) wraps DoPurchaseTwoStep: empty token falls through (CLI path preserved); non-empty token does the lookup first and short-circuits on match; lookup failure REFUSES to purchase rather than risk a double-buy (mirrors EC2 safety contract).

All 7 service executors (cache, compute, cosmosdb, database, managedredis, search, synapse) now call reservations.ApplyPurchaseTags(...) and reservations.DoIdempotentPurchaseTwoStep(...). Duplicated per-service applyPurchaseAutomationTag helpers deleted.

Tests (28 new)

  • Tag matrix: TestApplyPurchaseTags_BothTags / OnlySource / OnlyIdempotency / NoTags
  • Lookup: TestFindReservationOrderByIdempotencyToken_Match / NoMatch / SkipsTerminalFailed (×3) / AcceptsInFlightStates (×5) / HTTPError / 403 / EmptyToken / PaginatedFollowsNextLink
  • End-to-end: TestDoIdempotentPurchaseTwoStep_EmptyToken_NoLookup / NoMatch_FallsThroughToPurchase / **Match_ShortCircuits** (the #721 regression test — zero POST calls on re-drive) / LookupFailure_DoesNotPurchase / DifferentTokens_DistinctReservations / PreservesTwoStepFlow
  • Wiring: TestBuildReservationBody_IncludesIdempotencyTokenTag (compute-level)

Verification

  • go test ./... from providers/azure/: all 12 packages pass
  • go test ./internal/purchase/... from root: passes
  • go vet + gofmt + gocyclo (post-refactor) clean
  • Pre-commit clean (gofmt, vet, gocyclo, gosec, trivy)

Once this lands, #639 (full auto-re-drive flip across all providers) becomes safely actionable.

Summary by CodeRabbit

  • New Features

    • Added idempotency support for Azure reservation purchases to prevent duplicate charges on retries.
    • Implemented reservation lookup by idempotency token for reliable deduplication across service providers (Cache, Compute, CosmosDB, Database, Managed Redis, Search, Synapse).
  • Bug Fixes

    • Enhanced validation to require source attribution for reservation purchases.
  • Tests

    • Added comprehensive test coverage for idempotent purchase flows and tag application logic.

Review Change Stack

…eTwoStep

PR #680 (commit 65ecbf8) switched all 7 Azure reservation executors to
DoPurchaseTwoStep, which mints a fresh server-side reservationOrderId per
calculatePrice call and dropped the deterministic IdempotencyToken
threading PR #653 had added. A re-drive of the same (executionID, recIndex)
would create a SECOND Azure reservation, regressing #641's invariant.

Option A (client-supplied order ID) is not viable: Azure rejects
client-supplied IDs in the two-step flow (the very reason PR #680 had to
adopt it). The fix implements Option B from the issue, mirroring the AWS
EC2 findRIByIdempotencyToken pattern:

  * ApplyPurchaseTags now stamps two tags on every purchase body:
    purchase-automation (attribution, existing) and cudly-idempotency-token
    (new, the deterministic per-rec token).
  * FindReservationOrderByIdempotencyToken lists reservation orders via the
    Microsoft.Capacity REST endpoint, filters by tag, skips terminal-failed
    states (Cancelled/Failed/Expired), and follows nextLink pagination.
  * DoIdempotentPurchaseTwoStep wraps DoPurchaseTwoStep with the
    lookup-first/short-circuit guard. Empty token falls straight through
    (CLI legacy path). Failed lookups refuse to purchase rather than risk
    a double-buy (mirrors EC2 safety contract).

All 7 service executors (cache, compute, cosmosdb, database, managedredis,
search, synapse) now call DoIdempotentPurchaseTwoStep with opts.IdempotencyToken
and ApplyPurchaseTags with both opts.Source and opts.IdempotencyToken.

20 new tests cover both invariants: same token produces one reservation
(short-circuit), different tokens produce distinct reservations.
DoPurchaseTwoStep retains its previous behaviour for callers without an
owning execution. Per-service existing tests continue to pass unchanged.

Closes #721, partial-restore of #641 invariant for Azure, unblocks #639.
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/bug Defect labels May 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06e8a591-557a-4c8b-aaaa-a2d9705a279e

📥 Commits

Reviewing files that changed from the base of the PR and between 5abf40b and cc9383d.

📒 Files selected for processing (11)
  • providers/azure/services/cache/client.go
  • providers/azure/services/compute/client.go
  • providers/azure/services/compute/client_test.go
  • providers/azure/services/cosmosdb/client.go
  • providers/azure/services/database/client.go
  • providers/azure/services/internal/reservations/purchase.go
  • providers/azure/services/internal/reservations/purchase_test.go
  • providers/azure/services/managedredis/client.go
  • providers/azure/services/search/client.go
  • providers/azure/services/synapse/client.go
  • providers/azure/services/synapse/client_test.go

📝 Walkthrough

Walkthrough

Azure reservation purchase requests now include idempotency-token tags and undergo tag-based deduplication via a new two-step idempotent orchestrator applied across all seven service clients, with comprehensive test coverage validating the lookup, short-circuit, and fallthrough paths.

Changes

Azure Idempotent Reservation Purchases

Layer / File(s) Summary
Reservation idempotency infrastructure and tests
providers/azure/services/internal/reservations/purchase.go, providers/azure/services/internal/reservations/purchase_test.go
ReservationOrdersListURL() constructs the list-reservations endpoint. ApplyPurchaseTags() conditionally stamps source and idempotency-token tags into request bodies. FindReservationOrderByIdempotencyToken() paginates through reservation orders, matches by token, and skips terminal-failed states. DoIdempotentPurchaseTwoStep() short-circuits to existing order ID when lookup succeeds, returns lookup error to prevent double-buy, or falls through to non-idempotent two-step purchase. Comprehensive tests validate all tag combinations, match/no-match lookup behavior, pagination, error handling (403, network), and wrapper invariants.
Compute client idempotent purchase integration
providers/azure/services/compute/client.go, providers/azure/services/compute/client_test.go
buildReservationBody() signature now accepts idempotencyToken and applies tags via reservations.ApplyPurchaseTags(). PurchaseCommitment requires opts.Source (fail-fast when empty), passes both source and token to body-building, and calls reservations.DoIdempotentPurchaseTwoStep() with the token. Tests updated to pass token argument; new tests validate tag omission when both source and token are empty, and tag inclusion when token is supplied.
Cache, CosmosDB, Database, ManagedRedis, Search, Synapse client idempotent integration
providers/azure/services/cache/client.go, providers/azure/services/cosmosdb/client.go, providers/azure/services/database/client.go, providers/azure/services/managedredis/client.go, providers/azure/services/search/client.go, providers/azure/services/synapse/client.go, providers/azure/services/synapse/client_test.go
Each service's PurchaseCommitment enforces opts.Source is non-empty, calls reservations.ApplyPurchaseTags() with source and idempotency token, and executes via reservations.DoIdempotentPurchaseTwoStep() passing the token. Local applyPurchaseAutomationTag() helpers removed in favor of centralized tag application. Synapse test comments document the helper migration.

Sequence Diagram

sequenceDiagram
  participant ServiceClient
  participant PurchaseCommitment
  participant DoIdempotentPurchaseTwoStep
  participant FindReservationOrderByIdempotencyToken
  participant AzureListAPI
  participant DoPurchaseTwoStep
  participant AzurePurchaseAPI
  
  ServiceClient->>PurchaseCommitment: opts (source, idempotencyToken)
  PurchaseCommitment->>PurchaseCommitment: validate source non-empty
  PurchaseCommitment->>PurchaseCommitment: ApplyPurchaseTags(body, source, token)
  PurchaseCommitment->>DoIdempotentPurchaseTwoStep: calcURL, body, token
  
  alt Token present
    DoIdempotentPurchaseTwoStep->>FindReservationOrderByIdempotencyToken: lookup by token
    FindReservationOrderByIdempotencyToken->>AzureListAPI: list-reservation-orders
    alt Match found (non-terminal)
      AzureListAPI-->>FindReservationOrderByIdempotencyToken: order list
      FindReservationOrderByIdempotencyToken-->>DoIdempotentPurchaseTwoStep: existing orderID
      DoIdempotentPurchaseTwoStep-->>PurchaseCommitment: return orderID (short-circuit)
    else Lookup error
      AzureListAPI-->>FindReservationOrderByIdempotencyToken: error
      FindReservationOrderByIdempotencyToken-->>DoIdempotentPurchaseTwoStep: error
      DoIdempotentPurchaseTwoStep-->>PurchaseCommitment: abort with error
    else No match
      FindReservationOrderByIdempotencyToken-->>DoIdempotentPurchaseTwoStep: not found
      DoIdempotentPurchaseTwoStep->>DoPurchaseTwoStep: proceed to purchase
    end
  else Token absent
    DoIdempotentPurchaseTwoStep->>DoPurchaseTwoStep: skip lookup, direct purchase
  end
  
  DoPurchaseTwoStep->>AzurePurchaseAPI: calculate + purchase
  AzurePurchaseAPI-->>DoPurchaseTwoStep: new orderID
  DoPurchaseTwoStep-->>DoIdempotentPurchaseTwoStep: orderID
  DoIdempotentPurchaseTwoStep-->>PurchaseCommitment: orderID
  PurchaseCommitment-->>ServiceClient: result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#680: Introduced the original two-step reservation purchase flow that this PR now wraps with idempotency and tag-based deduplication.
  • LeanerCloud/CUDly#653: Earlier approach to Azure reservation idempotency using deterministic reservationOrderID derived from token; this PR uses server-side deduplication instead.
  • LeanerCloud/CUDly#638: Establishes per-execution IdempotencyToken threading and common.PurchaseOptions contract that this PR consumes to tag and dedupe Azure purchases.

Suggested labels

bug

Poem

🐰 Twice burned, the cloud learns to remember:
A token whispered, a list consulted,
Orders old and new reconciled—
No double-buy shall haunt these vaults.
Azure now sleeps soundly.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: restoring IdempotencyToken threading in the DoPurchaseTwoStep reservation purchase flow to fix the regression.
Linked Issues check ✅ Passed The PR fully implements the caller-level idempotency guard (Option B) required by issue #721, including ApplyPurchaseTags, FindReservationOrderByIdempotencyToken, and DoIdempotentPurchaseTwoStep across all seven Azure service executors with comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the idempotency fix for Azure two-step purchases: new reservation helper functions, updates to seven service executors, removal of duplicate local helpers, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/721-azure-two-step-idempotency

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant