Skip to content

fix(providers/azure): direct PUT to reservationOrders/{id} rejected by Azure ("Session timed out - Call CalculatePrice again") — switch to two-step CalculatePrice→Purchase flow (P0) #677

Description

@cristim

Symptom

QA reproduces with a freshly-created Azure purchase (minutes between create and approve, not a stale session):

Failed to approve: execution ed9b0f07-3e84-436a-b501-92d47b5fbb70 could not be approved:
some purchases failed: [Standard_B2ats_v2: purchase failed:
reservation purchase failed with status 400:
{"error":{"code":"BadRequest",
"message":"Session timed out - Call CalculatePrice again and provide the new Reservation Order ID for purchase"}}]

Standard_B2ats_v2 is a Burstable v2 VM SKU (a relatively new family). The error is returned by Azure's Reservations API at PUT time.

Root cause

Every Azure reservation-purchase client uses the direct-PUT pattern:

PUT https://management.azure.com/providers/Microsoft.Capacity/reservationOrders/{orderID}?api-version=2022-11-01
Body: { sku, location, properties{ reservedResourceType, billingScopeId, term, quantity, displayName, appliedScopeType, renew } }

The orderID is generated client-side (deterministic from idempotency token per #641, or a fresh UUID).

Azure now rejects this pattern for at least Burstable v2 VMs (and likely other newer SKU families) with the "Session timed out" error. The Reservations API is shifting to a two-step flow that requires the caller to first call calculatePrice to mint a session-bound order ID and a price quote, then call purchase with the quote ID.

This is a structural API contract change on Azure's side, not a bug in our code per se — but our code now produces 400s on real-world purchase attempts within minutes of creation.

Affected clients (all 6 reservation-based Azure services)

File Pattern
providers/azure/services/compute/client.go:466 Direct PUT
providers/azure/services/database/client.go:285 Direct PUT
providers/azure/services/cache/client.go:281 Direct PUT
providers/azure/services/search/client.go:254 Direct PUT
providers/azure/services/cosmosdb/client.go:277 Direct PUT
providers/azure/services/managedredis/client.go:259 Direct PUT

(compute/exchange.go references reservationOrders/{orderID}/reservations/{resID} but is the exchange flow, not the purchase flow — separate scope.)

Fix

Switch from the direct-PUT pattern to Azure's documented two-step flow:

  1. POST /providers/Microsoft.Capacity/calculatePrice?api-version=<v> with the reservation request body (sku, location, properties). Returns { properties: { reservationOrderId, paymentSchedule, billingCurrencyTotal, ... } }. The returned reservationOrderId is session-bound and short-lived (minutes).
  2. POST /providers/Microsoft.Capacity/reservationOrders/{reservationOrderId}/purchase?api-version=<v> to commit the order using the quote.

The session is short enough that the calculate → purchase round-trip must happen within the same handler invocation (no human-in-the-loop between). Our flow is: approve email click → executePurchase synchronous → calls Azure. Steps 1+2 fit within that single call without a human delay.

Idempotency: the new flow no longer accepts a client-supplied order ID; Azure mints one in step 1. To keep #641's idempotency guarantee:

  • Cache the (executionID, recIndex) → Azure-returned reservationOrderId mapping on the execution row's recommendations[i] JSONB (new field) on the FIRST calculate call.
  • On a re-drive, look up the cached order ID and skip straight to the purchase POST. If Azure has already retired that order ID (session expired), the purchase POST will return the "Session timed out" error and we re-run the calculate.
  • Alternative: skip the idempotency cache entirely and accept that re-drives create a new order ID each time, but always check via the purchaseURL whether the previous attempt's commitment already exists by tag before re-purchasing. Less clean but doesn't need a schema change.

Acceptance criteria

  • All 6 Azure reservation-purchase clients use the calculatePrice → purchase two-step flow
  • The change is verified end-to-end against the live Azure subscription for at least Standard_B2ats_v2 (the reported SKU) AND one other family to ensure the fix isn't SKU-specific
  • Idempotency story is preserved (feat(purchases): extend IdempotencyToken to all commitment executors (AWS RDS/ElastiCache/MemoryDB/OpenSearch/Redshift, Azure, GCP) — blocks #639 #641 invariant): a re-drive of the same execution does not create a duplicate Azure commitment
  • Unit tests cover: calculate-then-purchase happy path, calculate failure (4xx), purchase-after-calculate timeout (re-run calculate), idempotent re-drive
  • OAS / API-version pinning: pick the current GA api-version, document the choice in a comment

Severity

P0 / critical. Every freshly-created Azure VM reservation purchase to a Burstable v2 SKU (and likely other newer families) fails today. Customer impact is total: dashboard "Approve" button on those purchases is broken.

Cross-references

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions