All commitment executors except EC2 RI + Savings Plans silently ignore IdempotencyToken
internal/purchase/execution.go derives a deterministic recOpts.IdempotencyToken per rec (added by #636), but only two executors honor it:
Every other PurchaseCommitment implementation reads opts but never opts.IdempotencyToken, so a re-drive / scheduler retry creates a SECOND commitment:
AWS (5 services): RDS, ElastiCache, MemoryDB, OpenSearch, Redshift — confirmed zero IdempotencyToken reads across all five.
Azure: all reservation purchases mint a fresh random reservationOrderID (uuid.New() per call; the search client uses time.Now().Unix() at providers/azure/services/search/client.go:239). No IdempotencyToken read anywhere in providers/azure.
GCP: providers/gcp/services/computeengine/client.go:422 names commitments cud-<unix-second> and never reads opts.IdempotencyToken. (The other GCP services are separately broken — see the GCP-executor issue.)
Why this matters now
This is a financial double-purchase exposure on its own. It also blocks #639 (flip RecoverStrandedApprovals to idempotent auto-re-drive): the recovery sweep would re-drive a stranded approved row and double-purchase on every provider/service except EC2 RI + SP. #639 must not ship until this is closed.
Fix
Extend the #636 idempotency mechanism to every executor:
- AWS RDS/ElastiCache/MemoryDB/OpenSearch/Redshift: apply the EC2-style tag-guard (
DescribeReserved* filtered by tag:cudly-idempotency-token + active/payment-pending, short-circuit if found, tag after purchase) per service. Where the AWS API offers a native ClientToken, prefer it.
- Azure: derive
reservationOrderID deterministically from the IdempotencyToken (Azure reservation PUT is idempotent on a stable order ID) instead of uuid.New().
- GCP Compute: use the IdempotencyToken as the commitment name / requestId so a repeat is rejected/deduped.
Regression test per provider: same token on re-drive does not create a second commitment.
Dedup
#636/#639 are explicitly scoped to EC2 RI + SP only; #632 is the strand bug. The Azure cluster (#553-#596) covers SP/exchange/multi-account/pricing, not reservation-purchase idempotency. New. Surfaced in the full purchase-workflow trace.
All commitment executors except EC2 RI + Savings Plans silently ignore IdempotencyToken
internal/purchase/execution.goderives a deterministicrecOpts.IdempotencyTokenper rec (added by #636), but only two executors honor it:tag:cudly-idempotency-token(feat(purchases): idempotent commitment creation (ClientToken/dedupe) to enable auto-re-drive of stranded approvals #636).ClientToken(feat(purchases): idempotent commitment creation (ClientToken/dedupe) to enable auto-re-drive of stranded approvals #636).Every other
PurchaseCommitmentimplementation readsoptsbut neveropts.IdempotencyToken, so a re-drive / scheduler retry creates a SECOND commitment:AWS (5 services): RDS, ElastiCache, MemoryDB, OpenSearch, Redshift — confirmed zero
IdempotencyTokenreads across all five.Azure: all reservation purchases mint a fresh random
reservationOrderID(uuid.New()per call; the search client usestime.Now().Unix()atproviders/azure/services/search/client.go:239). NoIdempotencyTokenread anywhere inproviders/azure.GCP:
providers/gcp/services/computeengine/client.go:422names commitmentscud-<unix-second>and never readsopts.IdempotencyToken. (The other GCP services are separately broken — see the GCP-executor issue.)Why this matters now
This is a financial double-purchase exposure on its own. It also blocks #639 (flip
RecoverStrandedApprovalsto idempotent auto-re-drive): the recovery sweep would re-drive a strandedapprovedrow and double-purchase on every provider/service except EC2 RI + SP. #639 must not ship until this is closed.Fix
Extend the #636 idempotency mechanism to every executor:
DescribeReserved*filtered bytag:cudly-idempotency-token+ active/payment-pending, short-circuit if found, tag after purchase) per service. Where the AWS API offers a nativeClientToken, prefer it.reservationOrderIDdeterministically from the IdempotencyToken (Azure reservation PUT is idempotent on a stable order ID) instead ofuuid.New().Regression test per provider: same token on re-drive does not create a second commitment.
Dedup
#636/#639 are explicitly scoped to EC2 RI + SP only; #632 is the strand bug. The Azure cluster (#553-#596) covers SP/exchange/multi-account/pricing, not reservation-purchase idempotency. New. Surfaced in the full purchase-workflow trace.