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:
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).
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
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
Symptom
QA reproduces with a freshly-created Azure purchase (minutes between create and approve, not a stale session):
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:
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
calculatePriceto mint a session-bound order ID and a price quote, then callpurchasewith 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)
providers/azure/services/compute/client.go:466providers/azure/services/database/client.go:285providers/azure/services/cache/client.go:281providers/azure/services/search/client.go:254providers/azure/services/cosmosdb/client.go:277providers/azure/services/managedredis/client.go:259(
compute/exchange.goreferencesreservationOrders/{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:
POST /providers/Microsoft.Capacity/calculatePrice?api-version=<v>with the reservation request body (sku, location, properties). Returns{ properties: { reservationOrderId, paymentSchedule, billingCurrencyTotal, ... } }. The returnedreservationOrderIdis session-bound and short-lived (minutes).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:
recommendations[i]JSONB (new field) on the FIRST calculate call.Acceptance criteria
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