From bfbc9110bfa0382340a0faf881e3605cf23dd71d Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 28 May 2026 20:08:38 +0200 Subject: [PATCH] fix(azure/compute): ctx cancel terminal in SKU catalogue loop (closes #217) Treat context.Canceled/DeadlineExceeded as hard stops in the fetchSKUCatalogue pagination loop per feedback_ctx_cancel_terminal.md. VCPU/MemoryGB fall back to 0 on cancellation, matching the existing pager-error graceful-degradation contract. New test pins the invariant: a pre-cancelled context skips all NextPage calls and returns a nil catalogue. --- providers/azure/services/compute/client.go | 8 +++-- .../azure/services/compute/client_test.go | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/providers/azure/services/compute/client.go b/providers/azure/services/compute/client.go index 2b45d9a12..aeb974da9 100644 --- a/providers/azure/services/compute/client.go +++ b/providers/azure/services/compute/client.go @@ -798,9 +798,11 @@ func (c *ComputeClient) cachedSKULookup(ctx context.Context, skuName string) (vm // GetValidResourceTypes — a SKU listed for a different region is not // safe to attribute to a recommendation in this client's region). // -// Returns nil on pager-create or page-fetch error so the -// sync.Once-gated cache field stays nil and cachedSKULookup falls back -// to the empty-fields path. The fetch error is logged WARN once. +// Returns nil on pager-create, page-fetch error, or context cancellation +// so the sync.Once-gated cache field stays nil and cachedSKULookup falls +// back to the empty-fields path. Errors and cancellation are logged WARN +// once; context.Canceled/DeadlineExceeded are treated as terminal +// (feedback_ctx_cancel_terminal.md). func (c *ComputeClient) fetchSKUCatalogue(ctx context.Context) map[string]vmSKUEntry { pager, err := c.createResourceSKUsPager() if err != nil { diff --git a/providers/azure/services/compute/client_test.go b/providers/azure/services/compute/client_test.go index 579a81e29..6b991cd86 100644 --- a/providers/azure/services/compute/client_test.go +++ b/providers/azure/services/compute/client_test.go @@ -1083,6 +1083,35 @@ func TestComputeClient_ConvertAzureVMRecommendation_PagerErrorFallsBack(t *testi assert.Equal(t, 0.0, details.MemoryGB, "MemoryGB left at 0 when catalogue fetch fails") } +// TestComputeClient_FetchSKUCatalogue_CancelledContextFallsBack asserts +// that a cancelled context is terminal in the SKU catalogue pagination +// loop — the catalogue returns nil and Details.VCPU/MemoryGB stay at 0, +// but the conversion itself succeeds (graceful-degradation contract). +// Pins feedback_ctx_cancel_terminal.md for the compute SKU path. +func TestComputeClient_FetchSKUCatalogue_CancelledContextFallsBack(t *testing.T) { + client := NewClient(nil, "test-subscription", "eastus") + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately so ctx.Err() is set on first loop iteration + + mockPager := &vmSKUCatalogueMockPager{ + pages: []armcompute.ResourceSKUsClientListResponse{ + { + ResourceSKUsResult: armcompute.ResourceSKUsResult{ + Value: []*armcompute.ResourceSKU{ + buildVMSKU("Standard_D2s_v3", "eastus", 2, "8"), + }, + }, + }, + }, + } + client.SetResourceSKUsPager(mockPager) + + result := client.fetchSKUCatalogue(ctx) + assert.Nil(t, result, "cancelled context must return nil catalogue") + assert.Equal(t, 0, mockPager.pageHits, "NextPage must not be called after context is already cancelled") +} + // TestComputeClient_ConvertAzureVMRecommendation_NoMatchLeavesFieldsZero // asserts that when the recommendation's SKU isn't in the catalogue // (e.g. SKU listed for another region only), VCPU/MemoryGB stay at 0