You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Execution 9336a111-85c5-4b74-9991-367350de9bac (Lambda 60s budget, dev env, pre-#684) failed with:
EC2: DescribeReservedInstancesOfferings ... canceled, context deadline exceeded
AUDIT LOSS: failed to save execution status: failed to begin transaction: context deadline exceeded
CloudWatch report: Duration: 60000.77 ms -- the Lambda spent its entire budget paginating through DescribeReservedInstancesOfferings and was killed before it could even write the failed status (compounding into #632-style audit loss).
#667 diagnosed the same failure shape as an SDK-retry budget overrun and asserted "This is NOT a pagination issue -- buildOfferingFilters correctly narrows by ... so the result set is typically a single page." The CLI reproduction above falsifies that assertion: AWS returns empty pages with NextToken even for the narrow filter set, walking sparse offerings indefinitely. #684 (closing #667) caps the per-rec wall clock at 30s via context.WithTimeout and makes hangs observable, but the underlying lookup still spends the full budget; the rec fails every time it hits this offering layout. This issue is the lookup-correctness fix that #684's budget cap is masking.
Critical gap: the offering-type filter (payment option) is never set. AWS therefore returns every payment variant of every offering that matches the other filters, and the loop at client.go:367-398 then returns result.ReservedInstancesOfferings[0] from the first non-empty page without verifying its OfferingType matches the requested payment option.
Two failure modes follow:
Deep pagination on sparse offerings: AWS walks pages of empty results with NextToken until it lands on a matching offering set or the budget expires.
Wrong-variant return: if the first non-empty page contains a no-upfront variant when the rec asked for all-upfront (or vice versa), the wrong offering is purchased silently.
The same shape recurs across the sibling services that copy buildOfferingFilters-style logic: RDS, ElastiCache, MemoryDB, OpenSearch, Redshift, SavingsPlans. Each has its own filter syntax (Filters[] for EC2, direct fields for the others) but the same "filter set under-constrains the API" pattern likely applies.
Fix scope: sweep all 7 purchase-path services
For each of providers/aws/services/{ec2,rds,elasticache,memorydb,opensearch,redshift,savingsplans}/client.go:
Narrow the describe call to the minimum needed for a deterministic match. Each service's Describe*Offerings API supports filters beyond what we currently set; identify the maximally narrow filter set (region is implicit via the client's region; instance/node/cluster type, term, payment option, offering class, product description, tenancy/scope, etc. where applicable) and apply all of them.
Add the missing payment-option filter explicitly (EC2 offering-type, equivalents per service):
Rec value
AWS value
no-upfront
No Upfront
partial-upfront
Partial Upfront
all-upfront
All Upfront
Verify the returned offering's payment option matches before returning it (defense in depth -- even with the filter, the loop should reject mismatched variants instead of grabbing offerings[0]).
Cap pagination at a fixed page count (e.g. maxOfferingPages = 5 -- with MaxResults=100 that's up to 500 offerings) and fail fast with an explicit pagination cap reached after N pages error that names the rec. This converts "Lambda timeout + audit loss" into "fast diagnostic error + clean status save".
Honour ctx.Done() between pages so a parent-context cancellation surfaces immediately instead of waiting for the next AWS response.
Increase MaxResults from 100 to 1000 (the AWS max for these APIs) so fewer round-trips are needed when a match is buried.
Acceptance criteria
Every findOfferingID (or equivalent) across the 7 services applies the maximally narrow filter set, including the payment-option mapping
Each returns an error if AWS hands back an offering whose payment option does not match the rec
Each caps pagination at a fixed page count and emits a diagnostic error citing the rec details
Each checks ctx.Err() between pages
Regression test per service: mock returning N empty pages with NextToken -> assert fast failure with the cap error (not a context-deadline error)
Regression test per service: mock returning a non-matching offering on page 1 -> assert it is rejected (not silently purchased)
CloudWatch repro: t4g.nano rec on the dev env returns a deterministic error within seconds instead of timing out at 60s
Risk / blast radius
Touches all 7 purchase-path service clients. Each has unit tests; new tests are required.
The verification step (AWS + Azure: add read-only sanity checks + AWS RI exchange #3) tightens semantics: a previously-silent-wrong-variant purchase becomes a loud error. This is the correct behaviour but may surface real recs that have ambiguous PaymentOption strings.
Symptom
Execution
9336a111-85c5-4b74-9991-367350de9bac(Lambda 60s budget, dev env, pre-#684) failed with:CloudWatch report:
Duration: 60000.77 ms-- the Lambda spent its entire budget paginating throughDescribeReservedInstancesOfferingsand was killed before it could even write the failed status (compounding into #632-style audit loss).Reproduction via AWS CLI:
Distinction from #667
#667 diagnosed the same failure shape as an SDK-retry budget overrun and asserted "This is NOT a pagination issue -- buildOfferingFilters correctly narrows by ... so the result set is typically a single page." The CLI reproduction above falsifies that assertion: AWS returns empty pages with
NextTokeneven for the narrow filter set, walking sparse offerings indefinitely. #684 (closing #667) caps the per-rec wall clock at 30s viacontext.WithTimeoutand makes hangs observable, but the underlying lookup still spends the full budget; the rec fails every time it hits this offering layout. This issue is the lookup-correctness fix that #684's budget cap is masking.Root cause: offering lookup is under-constrained
providers/aws/services/ec2/client.go:320-346(buildOfferingFilters):Critical gap: the
offering-typefilter (payment option) is never set. AWS therefore returns every payment variant of every offering that matches the other filters, and the loop atclient.go:367-398then returnsresult.ReservedInstancesOfferings[0]from the first non-empty page without verifying its OfferingType matches the requested payment option.Two failure modes follow:
NextTokenuntil it lands on a matching offering set or the budget expires.The same shape recurs across the sibling services that copy
buildOfferingFilters-style logic: RDS, ElastiCache, MemoryDB, OpenSearch, Redshift, SavingsPlans. Each has its own filter syntax (Filters[] for EC2, direct fields for the others) but the same "filter set under-constrains the API" pattern likely applies.Fix scope: sweep all 7 purchase-path services
For each of
providers/aws/services/{ec2,rds,elasticache,memorydb,opensearch,redshift,savingsplans}/client.go:Narrow the describe call to the minimum needed for a deterministic match. Each service's
Describe*OfferingsAPI supports filters beyond what we currently set; identify the maximally narrow filter set (region is implicit via the client's region; instance/node/cluster type, term, payment option, offering class, product description, tenancy/scope, etc. where applicable) and apply all of them.Add the missing payment-option filter explicitly (EC2
offering-type, equivalents per service):no-upfrontNo Upfrontpartial-upfrontPartial Upfrontall-upfrontAll UpfrontVerify the returned offering's payment option matches before returning it (defense in depth -- even with the filter, the loop should reject mismatched variants instead of grabbing
offerings[0]).Cap pagination at a fixed page count (e.g.
maxOfferingPages = 5-- with MaxResults=100 that's up to 500 offerings) and fail fast with an explicitpagination cap reached after N pageserror that names the rec. This converts "Lambda timeout + audit loss" into "fast diagnostic error + clean status save".Honour
ctx.Done()between pages so a parent-context cancellation surfaces immediately instead of waiting for the next AWS response.Increase MaxResults from 100 to 1000 (the AWS max for these APIs) so fewer round-trips are needed when a match is buried.
Acceptance criteria
findOfferingID(or equivalent) across the 7 services applies the maximally narrow filter set, including the payment-option mappingctx.Err()between pagesNextToken-> assert fast failure with the cap error (not a context-deadline error)Risk / blast radius
Cross-references
WithTimeoutmakes this fail faster; this issue makes it not fail at all